网络编程——TCP编程

╄→гoц情女王★ 提交于 2019-11-28 11:08:20

前言

使用TCP通信时,TCP协议要求必须要有一个服务器端。这一点是由TCP协议本身的特性决定的,只要你使用TCP协议来通信,就必须要有一个TCP服务器端。

TCP服务器的大概工作过程

(1)服务器会使用专门“文件描述符”来监听客户的“三次握手”,然后建立连接。

(2)一旦连接建立成功后,服务器会分配一个专门的 “通信文件描述符”,用于实现与该连接客户的通信

由于建立连接时,双方的TCP协议都已经记住了对方IP和端口,所以双方正式通信时,TCP会自动使用记录的IP和端口,我们不需要重新指定对方的IP和端口。

TCP编程模型

在编程模型里面,必须要有一方是TCP服务器,另一方是TCP客户。服务器只有一个,但是客户端有很多,不管客户端有多少个,客户端与服务器端的通信,都按照编程模型的描述来实现的。

API

socket

原型

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 

功能

创建一个套接字文件,然后以文件形式来操作通信,不过套接字文件没有文件名。

参数

domian

作用:指定协议族

为什么要指定协议族?

因为你要使用的通信协议一定是属于某个协议族,所以如果不指定协议族,又怎么指定协议族中的某个具体协议呢。比如我们想用的是TCP协议,TCP属于TCP/IP协议族中的子协议,所以必须先通过domain指定TCP/IP协议族,不过TCP/IP协议族有两个版本,分别是IPV4是IPV6版本,我们目前使用的还是IPV4版本,因为Ipv6还未普及。IPV4是Internet Protocol Version4的缩写,直译为“网络协议第四版”,IPV4和IPV6这两个版本所使用的ip地址格式完全不同,IPV4:ip为32位 IPV6:ip为128位。不仅IPV4和IPV6的ip地址格式不同,其实所有不同网络协议族所使用ip地址格式都不相同。

domain可设置的常见宏值

可设置的有:AF_UNIX, AF_LOCAL、AF_INET、AF_INET6、AF_IPX、AF_NETLINK、AF_APPLETALK、AF_PACKET、AF_UNSPEC

AF是address family,表示地址家族的意思,由于每个网络协议族的ip地址格式完全不同,因此在指定ip地址格式时需要做区分,所以这些AF_***宏就是用于说明所使用的是什么协议的IP地址,这些个宏定义在了socket.h中,

#define AF_UNSPEC    0
#define AF_UNIX      1    /* Unix domain sockets       */
#define AF_LOCAL     1    /* POSIX name for AF_UNIX    */
#define AF_INET      2    /* Internet IP Protocol      */
#define AF_AX25      3    /* Amateur Radio AX.25       */
                     ... 

有人可能会说不对呀,domain是用来指定协议族的吗,但是AF_***确是用来区分不同协议ip格式的,给domain指定AF_***合适吗?

其实区分不同协议族应该使用PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC,PF就是protocol family的意思,意思是“协议家族”。PF_***与AF_***不同的只是前缀,不过AF_***与PF_***的值完全一样,比如AF_UNIX == PF_UNIX,所以给domain指定AF_***,与指定PF_***完全是一样的。

为什么AF_*** == PF_***?

AF_***用于区分不同协议族的ip地址格式,而PF_***则用于区分不同的协议族,但是每个协议族的IP格式就一种,所以协议族与自己的IP格式其实是一对一的,因此如果你知道使用的是什么ip地址格式,其实你也就知道了使用的是什么协议族,所以使用AF_***也可以用于区分不同的协议族。不过为了更加正规一点,区分不同协议族的宏还是被命名为了PF_***,只不过它的值就是AF_***的值。

#define PF_UNSPEC      AF_UNSPEC
#define PF_UNIX        AF_UNIX
#define PF_LOCAL       AF_LOCAL
#define PF_INET        AF_INET
#define PF_AX25        AF_AX25
            ...

domain是用于指定协议族的,设置的宏可以是AF_***,也可以是PF_***,不过正规一点的话还是应该写PF_***,因为这个宏才是专门用来区分协议族的,而AF_***则是用来区分不同协议族的ip地址格式的。不过socket的man手册里面写的都是AF_***,没有写PF_***。

domain的常见宏值,各自代表是什么协议族

PF_UNIX、PF_LOCAL:域通信协议族

这两个宏值是一样(宏值都是1)。给domain指定该宏时就表示,你要使用的是“本机进程间通信”协议族。域套接字的IPC,也可以专门用来实现“本机进程间通信”。这个域就是本机的意思,当我们给socket的domain指定这个宏时,创建的就是域套接字文件。

PF_INET:指定ipv4的TCP/IP协议族。

PF_INET6:ipv6的TCP/IP协议族,目前还未普及使用

PF_IPX:novell协议族,几乎用不到,了解即可

由美国Novell网络公司开发的一种专门针对局域网的“局域网通信协议”。这个协议的特点是效率较高,所以好多局域网游戏很喜欢使用这个协议来进行局域网通信,比如以前的局域网游戏CS,据说使用的就是novell协议族。之所以使用novell协议族,是因为CS的画面数据量太大,而且协同性要求很高,所以就选择了使用novell协议族这个高效率的局域网协议。现在互联网使用的都是TCP/IP协议,而novell和TCP/IP是两个完全不同的协议,所以使用novell协议族的局域网与使用TCP/IP协议族的互联网之间兼容性比较差,如果novell协议的局域网要接入TCP/IP的Internet的话,数据必须进行协议转换。所谓协议转换就是,novell局域网的数据包发向TCP/IP的互联网时,将novell协议族的数据包拆包,然后重新封包为TCP/IP协议的数据包。TCP/IP的互联网数据包发向novell局域网时,将TCP/IP协议族的数据包拆包,然后重新封包为novell协议的数据包。windows似乎并不是支持novell协议,但是Linux、unix这边是支持的。

PF_APPLETALK:苹果公司专为自己“苹果电脑”设计的局域网协议族。

AF_UNSPEC:不指定具体协议族

type

套接字类型,说白了就是进一步指定,你想要使用协议族中的那个子协议来通信。比如,如果你想使用TCP协议来通信,首先:将domain指定为PF_INET,表示使用的是IPV4的TCP/IP协议族其次:对type进行相应的设置,进一步表示我想使用的是TCP/IP协议族中的TCP协议。type的常见设置值:SOCK_STREAM、SOCK_DGRAM、SOCK_RDM、SOCK_NONBLOCK、SOCK_CLOEXEC

SOCK_STREAM:

将type指定为SOCK_STREAM时,表示想使用的是“有序的、面向连接的、双向通信的、可靠的字节流通信”,并且支持带外数据。

如果domain被设置为PF_INET,type被设置为SOCK_STREAM,就表示你想使用TCP来通信,因为在TCP/IP协议族里面,只有TCP协议是“有序的、面向连接的、双向的、可靠的字节流通信”。

使用TCP通信时TCP并不是孤立的,它还需要网络层和链路层协议的支持才能工作。

如果type设置为SOCK_STREAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似TCP这样的可靠传输协议。

SOCK_DGRAM:

将type指定为SOCK_DGRAM时,表示想使用的是“无连接、不可靠的、固定长度的数据报通信”。

固定长度意思是说,分组数据的大小是固定的,不管网络好不好,不会自动去调整分组数据的大小,所以“固定长度数据报”其实就是“固定长度分组数据”的意思。

当domain指定为PF_INET、type指定为SOCK_DGRAM时,就表示想使用的是TCP/IP协议族中的中的DUP协议,因为在TCP/IP协议族中,只有UDP是“无连接、不可靠的、固定长度的数据报通信”。

同样的UDP不可能独立工作,需要网络层和链路层协议的支持。

如果type设置为SOCK_DGRAM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似UDP这样的不可靠传输协议。

SOCK_RDM:

表示想使用的是“原始网络通信”。

比如,当domain指定为TCP/IP协议族、type指定为SOCK_RDM时,就表示想使用ip协议来通信,使用IP协议来通信,其实就是原始的网络通信。

为什么称为原始通信?

以TCP/IP协议族为例,TCP/IP之所以能够实现网络通信,最根本的原因是因为IP协议的存在,IP协议才是关键,只是原始的IP协议只能实现最基本的通信,还缺乏很多精细的功能,所以才多了基于IP工作的TCP和UDP,TCP/UDP弥补了IP缺失的精细功能。尽管ip提供的只是非常原始的通信,但是我们确实可以使用比较原始的IP协议来进通信,特别是当你不满意TCP和UDP的表现,你想实现一个比TCP和UDP更好的传输层协议时,你就可以直接使用ip协议,然后由自己的应用程序来实现符合自己要求的类似tcp/udp协议,不过我们几乎遇不到这种情况,这里了解下即可。如果type设置为SOCK_RDM,但是domain指定为了其它协议族,那就表示使用的是其它“协议族”中类似ip这样最基本的原始通信协议。

SOCK_NONBLOCK:

将socket返回的文件描述符指定为非阻塞的。

如果不指定这个宏的话,使用socket返回“套接字文件描述符”时,不管是是用来“监听”还是用来通信,都是阻塞操作的,但是指定这个宏的话,就是非阻塞的。当然也可以使用fcntl来指定SOCK_NONBLOCK,至于fcntl怎么用,参考高级IO。

SOCK_NONBLOCK宏可以和前面的宏进行 | 运算,比如:SOCK_STREAM | SOCK_NONBLOCK

SOCK_CLOEXEC:

表示一旦进程exec执行新程序后,自动关闭socket返回的“套接字文件描述符”。

这个标志也是可以和前面的宏进行 | 运算的,不过一般不指定这个标志。

SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC 

protocol

指定协议号。一般情况下protocol写0,表示使用domain和type所指定的协议。不过如果domain和type所对应的协议有好几个时,此时就需要通过具体的协议号来区分了,否者写0即可,表示domain和type所对应的协议就一个,不需要指定协议号来区分。

疑问:在哪里可以查到协议号?

所有协议的协议号都被保存在了/etc/protocols下。

协议       编号
ip         0 
icmp       1
igmp       2
tcp        6
udp        17
    等

返回值

成功:返回套接字文件描述符。 失败:返回-1,errno被设置

含义
EACCES 没有权限建立制定的domain的type的socket
EAFNOSUPPORT 不支持所给的地址类型
EINVAL 不支持此协议或者协议不可用
EMFILE 进程文件表溢出
ENFILE 已经达到系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM 内存不足。socket只有到资源足够或者有进程释放内存
EPROTONOSUPPORT 制定的协议type在domain中不存在

bind

原型

#include <sys/types.h>          
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能

将指定了通信协议(TCP)的套接字文件与IP以及端口绑定起来。

注意,绑定的一定是自己的Ip和端口,不是对方的,比如对于TCP服务器来说,绑定的就是服务器自己的ip和端口。

参数

sockfd:套接字文件描述符,代表socket创建的套接字文件。

addrlen:第二个参数所指定的结构体变量的大小

addr:

struct sockaddr结构体变量的地址,结构体成员用于设置你要绑定的ip和端口。

结构体成员:

struct sockaddr {
    sa_family_t sa_family;
    char  sa_data[14];
}

sa_family:指定AF_***,表示使用的什么协议族的IP,前面说过,协议族不同,ip格式就不同

sa_data:存放ip和端口

如果将ip和端口直接写入sa_data数组中,虽然可以做到,但是操作起来有点麻烦,不过好在,我们可以使用更容易操作的struct sockaddr_in结构体来设置。不过这个结构体在在bind函数的手册中没有描述。

struct sockaddr_in 
{
    sa_family_t            sin_family;   //设置AF_***(地址族)
    __be16                sin_port;       //设置端口号
    struct in_addr        sin_addr;           //设置Ip
                                            
    /* 设置IP和端口时,这个成员用不到,这个成员的作用后面再解释, */
    unsigned char        __pad[__SOCK_SIZE__ - sizeof(short int) - 
                                                sizeof(unsigned short int) - sizeof(struct in_addr)];
};struct in_addr {   __be32 s_addr; //__be32是32位的unsigned int,因为IPV4的ip是32位的无符号整形数 };

在struct sockaddr_in中,存放端口和ip的成员是分开的,所以设置起来很方便。使用struct sockaddr_in设置后,然后将其强制转为struct sockaddr类型,然后传递给bind函数即可。

struct sockaddr_in的使用例子:

struct sockaddr_in addr;
                                    
addr.sin_family      = AF_INET;    //使用是IPV4 TCP/IP协议族的ip地址(32位)
addr.sin_port = htons(5006);    /指定端口
addr.sin_addr.s_addr = inet_addr("192.168.1.105");  //指定ip地址
                                    
ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

注意,如果是跨网通信时,绑定的一定是所在路由器的公网ip。bind会将sockfd代表的套接字文件与addr中设置的ip和端口绑定起来。

返回值

成功返回0,失败返回-1,errno被设置。

|值           |    含义                             |     备注         |
|-----------  |------------------------------------|-----------------|
|EADDRINUSE   |给定地址已经使用,实际上是端口被使用
|EBADF        |sockfd不合法    
|EINVAL       |sockfd已经绑定到其他地址    
|ENOTSOCK     |sockfd是一个文件描述符,不是socket描述符    
|EACCES       |地址被保护,用户的权限不足    
|EADDRNOTAVAIL|接口不存在或者绑定地址不是本地           |UNIX协议族,AF_UNIX
|EFAULT       |my_addr指针超出用户空间                |UNIX协议族,AF_UNIX
|EINVAL       |地址长度错误,或者socket不是AF_UNIX族   |UNIX协议族,AF_UNIX
|ELOOP        |解析my_addr时符号链接过多              |UNIX协议族,AF_UNIX
|ENAMETOOLONG |my_addr过长                          |UNIX协议族,AF_UNIX
|ENOENT       |文件不存在                            |UNIX协议族,AF_UNIX
|ENOMEN       |内存内核不足                          |UNIX协议族,AF_UNIX
|ENOTDIR      |不是目录                             |UNIX协议族,AF_UNIX

 

到底什么是绑定?

所谓绑定就是让套接字文件在通信时,使用固定的IP和端口。

对于TCP的服务器来说,必须绑定。

对于TCP通信的客户端来说,自动指定ip和端口是常态。客户与服务器建立连接时,服务器会从客户的数据包中提取出客户的ip和端口,并保存起来。

htons

原型 

#include <arpa/inet.h>                                
uint16_t htons(uint16_t hostshort); 

功能

功能有两个

1. 将端口号从“主机端序”转为“网络端序”

2. 如果给的端口不是short,将其类型转为short型

htons:是host to net short的缩写,

host:主机端序,主机端序可能是大端序,也可能是小端序,视OS而定

net:网络端序,网络端序都是固定使用大端序

short:短整形

参数

hostshort:主机端序的端口号

返回值

该函数的调用永远都是成功的,返回转换后的端口号

htons的兄弟函数

htonl、ntohs、ltohs

htonl:与htons唯一的区别是,转完的端口号时long

ntohs:htons的相反情况,网络端序转为主机端序

ntohl:htonl的相反情况

有关端口号的数值问题

三个范围:0~1023、1024~49151、49152~65535。

0~1023:这个范围的端口最好不要用,因为这个范围的端口已经被世界公认的各种服务征用了,比如80就被web服务征用了,所以所有web服务器程序的端口都是固定的80。

1024~49151:自己实现服务器程序,建议使用这个范围的端口号

49152~65535:用于自动分配的,一般客户端程序不会绑定固定的ip和端口,因为客户端的Ip和端口都是自动分配的,在自动分配端口时,所分配的就是49152~65535范围的端口。

inet_addr

原型

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); 

功能

功能有2个

1. 将字符串形式的Ip"192.168.1.105"(点分十进制),转为IPV4的32位无符号整形数的ip 

2. 将无符号整形数的ip,从主机端序转为网络端序

参数

cp:字符串形式的ip

返回值

永远成功,返回网络端序的、32位无符号整形数的ip。

listen

原型 

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog); 

功能

将套接字文件描述符,从主动文件描述符变为被动描述符,然后用于被动监听客户的连接。

不要因为listen有听的意思,就想当然的认为,listen就是用于被动监听客户连接的函数,事实上真正用于被动监听客户连接的函数,并不是listen,而是其它函数。listen的作用仅仅只是用于将“套接字文件描述符”变成被动描述符,以供“监听函数”用于被动监听客户连接而已。

参数

sockfd

socket所返回的套接字文件描述符。socket返回的“套接字文件描述符”默认是主动的,如果你想让它变为被动的话,你需要自己调用listen函数来实现。

backlog

指定队列的容量。这个队列用于记录正在连接,但是还没有连接完成的客户,一般将队列容量指定为2、3就可以了。这个容量并没有什么统一个设定值,一般来说只要小于30即可。

返回值

成功返回0,失败返回-1,ernno被设置

|值          |                  含义                |
|------------|--------                             |
| EADDRINUSE |另一个套接字已经绑定在相同的端口上。       |
|EBADF       |参数sockfd不是有效的文件描述符。         |
|ENOTSOCK    |参数sockfd不是套接字。                  |
|EOPNOTSUPP  |参数sockfd不是支持listen操作的套接字类型。|

 

主动描述符 & 被动描述符

主动描述符可以主动的向对方发送数据。

被动描述符只能被动的等别人主动想你发数据,然后再回答数据,不能主动的发送数据。

accept

原型 

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能

被动监听客户发起三次握手的连接请求,三次握手成功,即建立连接成功。

accept被动监听客户连接的过程,其实也被称为监听客户上线的过程。对于那些只连接了一半,还未连接完成的客户,会被记录到未完成队列中,队列的容量由listen函数的第二个参数(backlog)来指定。服务器调用accept函数监听客户连接,而客户端则是调用connect来连接请求的。一旦连接成功,服务器这边的TCP协议会记录客户的IP和端口。

参数

sockefd

已经被listen转为了被动描述符的“套接字文件描述符”,专门用于被动监听客户的连接。如果sockfd没有被listen转为被动描述符的话,accept是无法将其用来监听客户连接的。

有关套接字描述符的阻塞与非阻塞问题?

服务器程序调用socket得到“套接字文件描述符”时,如果socket的第2个参数type,没有指定SOCK_NONBLOCK的话,“套接字文件描述符”默认就是阻塞的,所以accept使用它来监听客户连接时,如果没有客户请求连接的话,accept函数就会阻塞,直到有客户连接位置。如果你不想阻塞,我们就可以在调用socket时,给type指定SOCK_NONBLOCK宏。

addrlen

第二参数addr的大小,不过要求给的是地址。

addr

用于记录发起连接请求的那个客户的IP和端口(port)。

如果服务器应用层需要用到客户ip和端口的话,可以给accept指定第二个参数addr,以获取TCP在连接时所自动记录客户IP和端口,如果不需要的就写NULL。

addr为struct sockaddr类型,虽然下层(内核)实际使用的是struct sockaddr结构体,但是由于这个结构体用起来不方便,因此应用层会使用更加便于操作的结构体,比如使用TCP/IP协议族通信时,应用层使用的就是struct sockaddr_in这个更加方便操作的结构体。

所以我们应该定义struct sockaddr_in类型的addr,传递给accept函数时,将其强制转为struct sockaddr即可,与我们讲bind函数时的用法类似。

struct sockaddr_in clnaddr = {0};                                
int clnsize = sizeof(clnaddr)
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnsize);

返回值

成功:返回一个通信描述符,专门用于与该连接成功的客户的通信,总之后续服务器与该客户间正式通信,使用的就是accept返回的“通信描述符”来实现的。

失败:返回-1,errno被设置

|         值      |              含义                 |
|----------------|-----------------------------------|
|EBADF           |非法的socket                        |
|EFAULT          |参数addr指针指向无法存取的内存空间      |
|ENOTSOCK        |参数s为一文件描述词,非socket          |
|EOPNOTSUPP      |指定的socket并非SOCK_STREAM          |
|EPERM           |防火墙拒绝此连线                      |
|ENOBUFS         |系统的缓冲内存不足                    |
|ENOMEM          |核心内存不足                         |

 

如何使用得到的客户ip和端口 

struct sockaddr_in clnaddr = {0};
int clnaddr_size = sizeof(clnaddr)
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size);                        
printf("cln_port= %d, cln_addr=%s\n", ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr));

 

服务器调用read(recv)和write(send),收发数据,实现与客户的通信

read和write的用法,在文件IO时已经介绍的非常清楚,我们这里着重介绍recv和send这两个函数,recv和send其实和read和write差不多,它们的前三个参数都是一样的,只是recv和send多了第四个参数。不管是使用read、write还是使用recv、send来实现TCP数据的收发,由于TCP建立连接时自动已经记录下了对方的IP和端口,所以使用这些函数实现数据收发时,只需要指定通信描述符即可,不需要指定对方的ip和端口。

send

原型 

#include <sys/types.h>
#include <sys/socket.h>                             
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 

功能

向对方发送数据

其实也可以使用sendto函数,相比send来说多了两个参数,当sendto的后两个参数写NULL和0时,能完全等价于send。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, NULL, 0);

类似TCP这种面向连接的通信,我们一般使用send而不是使用sendto,因为sendto用起来有点麻烦。类似UDP这种不需要连接的通信,必须使用sendto,不能使用send。

参数

sockefd

用于通信的通信描述符。不要因为名字写的是sockfd,就认为一定是socket返回“套接字文件描述符”。在服务器这边,accept返回的才是通信描述符,所以服务器调用send发送数据时,第一个参数应该写accept所返回的通信描述符。

buf:

应用缓存,用于存放你要发送的数据。可以是任何你想发送的数据,比如结构体、int、float、字符、字符串等等。正规操作的话,应该使用结构体来封装数据。

len:

buf缓存的大小

flags:

0:表示用不上flags,此时send是阻塞发送数据的。阻塞发送的意思就是,如果数据发送不成功会一直阻塞,直到被某信号中断或者发送成功为止,不过一般来说,发送数据是不会阻塞的。当flags设置0时,send与write的功能完全一样。

MSG_NOSIGNAL:send数据时,如果对方将“连接”关闭掉了,调用send的进程会被发送SIGPIPE信号,这个信号的默认处理方式是终止,所以收到这个信号的进程会被终止。如果给flags指定MSG_NOSIGNAL,表示当连接被关闭时不会产生该信号。从这里可看出,并不是只有写管道失败时才会产生SGIPIPE信号,网络通信时也会产生这个的信号。

MSG_DONTWAIT:非阻塞发送

MSG_OOB:表示发送的是带外数据

以上除了0以外,其它选项可以|操作,比如MSG_DONTWAIT | MSG_OOB。

返回值

成功返回发送的字节数,失败返回-1,ernno被设置

recv

原型 

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 

功能

接收对方发送的数据。我们也可以使用rcvfrom函数,当recvfrom函数的最后两个参数写NULL和0时,与recv的功能完全一样。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, NULL, 0);

参数

sockfd:通信文件描述符

buf:应用缓存,用于存放接收的数据

len:buf的大小

flags

0:默认设置,此时recv是阻塞接收的,0是常设置的值。

MSG_DONTWAIT:非阻塞接收

MSG_OOB:接收的是带外数据

返回值

成功返回接收的字节数,失败返回-1,ernno被设置

调用close或者shutdown关闭TCP的连接

TCP断开连接时,可以由客户和服务器任何一方发起。调用close或者sutdown函数断开连接时,四次握手的过程是由TCP自动完成的。

close

原型 

#include <unistd.h>
int close(int fd); 

功能

关闭文件描述符

参数

fd:文件描述符

返回值

成功返回0,失败返回-1,ernno被设置

close断开连接的缺点

缺点1:会一次性将读写都关掉了。如果我只想关写,但是读打开着,或者只想关读、但是写打开着,close做不到。

缺点2:如果多个文件描述符指向了同一个连接时,如果只close关闭了其中某个文件描述符时,只要其它的fd还打开着,那么连接不会被断开,直到所有的描述符都被close后才断开连接。

出现多个描述指向同一个连接的原因可能两个:

通过dup方式复制出其它描述符

子进程继承了这个描述符,所以子进程的描述符也指向了连接

shutdown

原型 

#include <sys/socket.h>
int shutdown(int sockfd, int how); 

功能

可以按照要求关闭连接,而且不管有多少个描述符指向同一个连接,只要调用shutdown去操作了其中某个描述符,连接就会被立即断开。

参数

sokcfd:TCP服务器断开连接时,使用的是accept所返回的文件描述符

how:如何断开连接

SHUT_RD:只断开读连接

SHUT_WR:只断开写连接

SHUT_RDWR:读、写连接都断开

返回值

成功返回0,失败返回-1,ernno被设置

代码演示

client.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <strings.h>
  6 #include <sys/types.h>          /* See NOTES */
  7 #include <sys/socket.h>
  8 #include <errno.h>
  9 #include <sys/socket.h>
 10 #include <netinet/in.h>
 11 #include <arpa/inet.h>
 12 #include <pthread.h>
 13 #include <signal.h>
 14 
 15 
 16 #define SPORT 5006
 17 #define SIP "192.168.1.106"
 18 
 19 /* 封装应用层数据, 目前要传输的是学生数据 */
 20 //学生信息
 21 typedef struct data
 22 {
 23     unsigned int stu_num;
 24     char stu_name[50];
 25 }Data;
 26 
 27 void print_err(char *str, int line, int err_no)
 28 {
 29         printf("%d, %s: %s\n", line, str, strerror(err_no));
 30         exit(-1);
 31 }
 32 
 33 int sockfd = -1;
 34 
 35 void *pth_fun(void *pth_arg)
 36 {
 37     int ret = 0;
 38     Data stu_data = {0};
 39     
 40     while(1)
 41     {
 42         bzero(&stu_data, sizeof(stu_data));
 43         
 44         ret = recv(sockfd, (void *)&stu_data, sizeof(stu_data), 0);
 45         if(ret > 0)
 46         {
 47             printf("student number:%d\n", ntohl(stu_data.stu_num));
 48             printf("student name:%s\n", stu_data.stu_name);
 49         }
 50         else if(ret == -1) print_err("recv fail", __LINE__, errno);
 51         
 52     }
 53 }
 54 
 55 void signal_fun(int signo)
 56 {
 57     if(SIGINT == signo)
 58     {
 59         /* 断开连接 */
 60         //close(sockfd);
 61         shutdown(sockfd);
 62 
 63         exit(0);
 64     }
 65 }
 66 
 67 int main(void)
 68 {
 69     int ret = 0;
 70 
 71     signal(SIGINT, signal_fun);
 72 
 73     /* 创建套接字文件,并指定使用TCP协议
 74      * 对于客户端的套接字文件描述符来说,直接用于通信 */
 75     sockfd = socket(PF_INET, SOCK_STREAM, 0);
 76     if(sockfd == -1) print_err("socket fail", __LINE__, errno);
 77     
 78     /* 调用connect,想服务器主动请求连接 */
 79     struct sockaddr_in seraddr = {0};//用于存放你要请求连接的那个服务器的ip和端口
 80     
 81     seraddr.sin_family = AF_INET;//地址族
 82     seraddr.sin_port   = htons(SPORT);//服务器程序的端口
 83     seraddr.sin_addr.s_addr = inet_addr(SIP);//服务器的ip,如果是跨网通信的话,就是服务器的公网ip
 84      
 85     ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
 86     if(ret == -1) print_err("connect fail", __LINE__, errno);
 87 
 88     pthread_t tid;
 89     ret = pthread_create(&tid, NULL, pth_fun, NULL);
 90     if(ret != 0) print_err("pthread_create fail", __LINE__, ret);
 91 
 92 
 93     Data stu_data  = {0};
 94     int tmp_num = 0;
 95     while(1)
 96     {
 97         bzero(&stu_data, sizeof(stu_data));
 98         /* 封入学生学号 */
 99         printf("input student number\n");    
100         scanf("%d", &tmp_num);
101         stu_data.stu_num = htonl(tmp_num);
102         
103         /* 封如学生名字 */
104         printf("input student name\n");
105         scanf("%s", stu_data.stu_name);
106         
107         ret = send(sockfd, (void *)&stu_data, sizeof(stu_data), 0);
108         if(ret == -1) print_err("send fail", __LINE__, errno);
109     }
110 
111     return 0;
112 }
View Code

server.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <strings.h>
  6 #include <sys/types.h>          /* See NOTES */
  7 #include <sys/socket.h>
  8 #include <errno.h>
  9 #include <sys/socket.h>
 10 #include <netinet/in.h>
 11 #include <arpa/inet.h>
 12 #include <pthread.h>
 13 #include <signal.h>
 14 
 15 
 16 #define SPORT 5006
 17 #define SIP "192.168.1.106"
 18 
 19 /* 封装应用层数据, 目前要传输的是学生数据 */
 20 //学生信息
 21 typedef struct data
 22 {
 23     unsigned int stu_num;
 24     char stu_name[50];
 25 }Data;
 26 
 27 void print_err(char *str, int line, int err_no)
 28 {
 29         printf("%d, %s: %s\n", line, str, strerror(err_no));
 30         exit(-1);
 31 }
 32 
 33 int cfd = -1;//存放与连接客户通信的通信描述符
 34 
 35 void signal_fun(int signo)
 36 {
 37     if(signo == SIGINT)
 38     {    
 39         //断开连接
 40         //close(cfd);
 41         shutdown(cfd, SHUT_RDWR);
 42         
 43         exit(0);
 44     }
 45 }
 46 
 47 
 48 /* 此线程接收客户端的数据 */
 49 void *pth_fun(void *pth_arg)
 50 {
 51     int ret = 0;
 52     Data stu_data = {0};
 53         
 54     while(1)
 55     {
 56         bzero(&stu_data, sizeof(stu_data));
 57         //ret = read(cfd, &stu_data, sizeof(stu_data));
 58         ret = recv(cfd, &stu_data, sizeof(stu_data), 0);
 59         if(ret == -1) print_err("recv fail", __LINE__, errno);
 60         else if(ret > 0)
 61         {    
 62             printf("student number = %d\n", ntohl(stu_data.stu_num));
 63             printf("student name = %s\n", stu_data.stu_name);
 64         }
 65     }
 66 }
 67 
 68 int main(void)
 69 {
 70     int ret = -1;
 71     int sockfd = -1;
 72     
 73 
 74     signal(SIGINT, signal_fun);
 75 
 76     /* 创建使用TCP协议通信的套接字文件 */
 77     sockfd = socket(PF_INET, SOCK_STREAM, 0);
 78     if(sockfd == -1) print_err("socket fail", __LINE__, errno);
 79 
 80     /* 调用Bind绑定套接字文件/ip/端口 */
 81     struct sockaddr_in saddr;
 82     saddr.sin_family = AF_INET;//制定ip地址格式(地址族)
 83     saddr.sin_port      = htons(SPORT);//服务器端口
 84     saddr.sin_addr.s_addr = inet_addr(SIP);//服务器ip
 85     
 86     ret = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
 87     if(ret == -1) print_err("bind fail", __LINE__, errno);
 88 
 89     /* 讲主动的"套接字文件描述符"转为被动描述符,用于被动监听客户的连接 */
 90     ret = listen(sockfd, 3);
 91     if(ret == -1) print_err("listen fail", __LINE__, errno);
 92     
 93     /* 调用accept函数,被动监听客户的连接 */
 94     struct sockaddr_in clnaddr = {0};//存放客户的ip和端口
 95     int clnaddr_size = sizeof(clnaddr);
 96 
 97     cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size);
 98     if(cfd == -1) print_err("accept fail", __LINE__, errno);
 99     //打印客户的端口和ip, 一定要记得进行端序转换
100     printf("clint_port = %d, clint_ip = %s\n", ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr));
101 
102     /* 创建一个次线程,用于接受客户发送的数据 */
103     pthread_t tid;
104     ret = pthread_create(&tid, NULL, pth_fun, NULL);
105     if(ret != 0) print_err("pthread_create fail", __LINE__, ret);    
106 
107     Data stu_data = {0};
108     int tmp_num;
109     while(1)
110     {
111         bzero(&stu_data, sizeof(stu_data));
112         /* 获取学生学号,但是需要讲让从主机端序转为网络端序 */
113         printf("input student number\n");
114         scanf("%d", &tmp_num);
115         stu_data.stu_num = htonl(tmp_num);
116         
117         /* char的数据不需要进行端序的转换 */
118         printf("input student name\n");
119         scanf("%s", stu_data.stu_name);
120     
121         /* 发送数据 */    
122         //ret = write(cfd, (void *)stu_data, sizeof(stu_data));
123         ret = send(cfd, (void *)&stu_data, sizeof(stu_data), 0);
124         if(ret == -1) print_err("send fail", __LINE__, errno);    
125     }
126     
127 
128     return 0;
129 }
View Code

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!