前言
使用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 }
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 }
来源:https://www.cnblogs.com/kelamoyujuzhen/p/9458762.html