1.前言
本篇文章的所有例子,基于RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)。
在前一篇文章中(点此链接),已经介绍了socket(),bind(),listen(),connect(),accept()这些函数。
至此,服务器与客户机已经建立好了连接。可以调用网络I/O进行读写操作了,即实现网络中不同进程之间的通信。网络I/O操作有下面的几组函数:
· read() / write()
· readv() / writev()
· send() / recv()
· sendmsg() / recvmsg()
· sendto() / recvfrom()
下面的几个小节会对这些函数进行详细介绍。
2.read()-write()函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read函数负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,<0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的count字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有2种可能:
1)write的返回值大于0,表示写了部分或者是全部的数据。
2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
3.readv()-writev()函数
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
参数fd是文件描述字。
参数iov是一个结构数组,它的每个元素指明存储器中的一个缓冲区。结构类型iovec有下述成员,分别给出缓冲区的起始地址和字节数。
参数iovcnt指出数组iov的元素个数,元素个数至多不超过IOV_MAX。Linux中定义IOV_MAX的值为1024。
UNIX提供了另外两个函数—readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。
readv()称为散布读(scatter read),即将文件中若干连续的数据块读入内存分散的缓冲区中。
writev()称为聚集写(gather write),即收集内存中分散的若干缓冲区中的数据写至文件的连续区域中。
下图说明了参数iovcnt、iov及其所指数组与这两个函数的关系。
writev()依次将iov[0]、iov[1]、...、 iov[iovcnt–1]指定的存储区中的数据写至fd指定的文件。writev()的返回值是写出的数据总字节数,正常情况下它应当等于所有数据块长度之和。
readv()则将fd指定文件中的数据按iov[0]、iov[1]、...、iov[iovcnt–1]规定的顺序和长度,分散地读到它们指定的存储地址中。readv()的返回值是读入的总字节数。如果没有数据可读和遇到了文件尾,其返回值为0。
有了这两个函数,当想要集中写出某张链表时,只需让iov数组的各个元素包含链表中各个表项的地址和其长度,然后将iov和它的元素个数作为参数传递给writev(),这些数据便可一次写出。
4.send()-recv()函数
#include <sys/types.h>recv 和send的前3个参数等同于read和write。flags参数值为0或下面列表所示的选项:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
flags | 说明 | recv | send |
MSG_DONTROUTE | 绕过路由表查找 | • | |
MSG_DONTWAIT | 仅本操作非阻塞 | • | • |
MSG_OOB | 发送或接收带外数据 | • | • |
MSG_PEEK | 窥看外来消息 | • | |
MSG_WAITALL | 等待所有数据 | • |
4.1.send()函数说明
sockfd:指定发送端套接字描述符。buf: 存放要发送数据的缓冲区len: 实际要发送数据的字节数flags: 一般设置为01) send先比较发送数据的长度len和套接字sockfd的发送缓冲区的长度。 如果len > 套接字sockfd的发送缓冲区的长度,该函数返回SOCKET_ERROR;
2) 如果len <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据。如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和len;
3) 如果len > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完;
4) 如果len < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲区中的数据传输到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里);
5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
6) send函数把buf中的数据成功copy到sockfd的发送缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传输到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR(每一个除send的socket函数在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传输完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)。
7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。
4.2.recv()函数说明
sockfd: 接收端套接字描述符buf: 用来存放recv函数接收到的数据的缓冲区len: 指明buf的长度flags: 一般置为01) recv先等待sockfd的发送缓冲区的数据被协议传送完毕,如果协议在传送sockfd的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
2) 如果套接字sockfd的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一起等待,直到把数据接收完毕。当协议把数据接收完毕,recv函数就把sockfd的接收缓冲区中的数据copy到buf中(注意:协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把sockfd的接收缓冲区中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)。
3) recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR。如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
4) 在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用 recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
5.sendmsg()-recvmsg()函数
#include <sys/types.h>参数sockfd:文件描述符
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr
{
void *msg_name; /* 消息的协议地址 */
socklen_t msg_namelen; /* 地址的长度 */
struct iovec *msg_iov; /* 多io缓冲区的地址 */
size_t msg_iovlen; /* 缓冲区的个数 */
void *msg_control; /* 辅助数据的地址 */
socklen_t msg_controllen; /* 辅助数据的长度 */
int msg_flags; /* 接收消息的标识 */
};
参数msg:存放消息头的内存缓冲
参数flags:是以下0个或者多个标志的组合体,可通过or操作连在一起。
MSG_DONTROUTE:不要使用网关来发送封包,只发送到直接联网的主机。这个标志主要用于诊断或者路由程序。
MSG_DONTWAIT:操作不会被阻塞。
MSG_EOR:终止一个记录。
MSG_MORE:调用者有更多的数据需要发送。
MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。
MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持此种操作。
sendmsg和recvmsg这两个接口是高级套接口,这两个接口支持一般数据的发送和接收,还支持多缓冲区的报文发送和接收(readv和sendv也支持多缓冲区发送和接收),还可以在报文中带辅助数据。这些功能是常用的send、recv等接口无法完成的。
关于这两个函数的具体用法,可以参考本系列的后续文章。
6.sendto()-recvfrom()函数
#include <sys/types.h>这两个函数一般用于UDP协议中。但是如果在TCP中connect函数调用后也可以使用。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
6.1.sendto()函数说明
在无连接的数据报socket方式下,由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址,sendto函数比send函数多了两个参数,dest_addr表示目地机的IP地址和端口号信息,而addrlen常常被赋值为sizeof(struct sockaddr)。sendto()函数返回值为实际发送的数据字节长度或在出现发送错误时返回-1。
sendto()函数主要用于SOCK_DGRAM类型套接口向dest_addr参数指定端的套接口发送数据报。对于SOCK_STREAM类型套接口,dest_addr和addrlen参数会被忽略;这种情况下sendto()等价于send()。
对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。如果数据太长无法自动通过下层协议,则返回EMSGSIZE错误,数据不会被发送。
另外需要注意的是,成功地完成sendto()调用并不意味着数据传送到达。
如果发送端的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则sendto()将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select()调用来确定何时能够进一步发送数据。
6.2.recvfrom()函数说明
Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
对于SOCK_STREAM类型的套接口,最多可接收缓冲区大小个数据。如果套接口被设置为线内接收带外数据(选项为SO_OOBINLINE),且有带外数据未读入,则返回带外数据。应用程序可通过调用ioctlsocket()的SOCATMARK命令来确定是否有带外数据待读入。对于SOCK_STREAM类型套接口,忽略from和fromlen参数。
对于SOCK_DGRAM类型的套接口,队列中第一个数据报中的数据被解包,但最多不超过缓冲区的大小。如果数据报大于缓冲区,那么缓冲区中只有数据报的前面部分,其他的数据都丢失了,并且recvfrom()函数返回EMSGSIZE错误。
若from非零,且套接口为SOCK_DGRAM类型,则发送数据源的地址被复制到相应的sockaddr结构中。fromlen所指向的值初始化时为这个结构的大小,当调用返回时按实际地址所占的空间进行修改。
如果没有数据待读,那么除非是非阻塞模式,不然的话套接口将一直等待数据的到来,此时将返回SOCKET_ERROR错误,错误代码是EWOULDBLOCK。用select()或poll()可以获知何时数据到达。
如果套接口为SOCK_STREAM类型,并且远端“优雅”地中止了连接,那么recvfrom()一个数据也不读取,立即返回。如果立即被强制中止,那么recv()将以ECONNRESET错误失败返回。
在套接口的所设选项之上,还可用标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口选项,也取决于标志位参数。标志位可取下列值:
MSG_PEEK ---- 查看当前数据。数据将被复制到缓冲区中,但并不从输入队列中删除。
MSG_OOB ---- 处理带外数据。
7.close()函数
TCP关闭连接有2种方式,一种是关闭端发送FIN,对方回应FIN+ACK,关闭端再回ACK,这是优雅的关闭连接。双方可以保证所有数据都发送接收完成了。另一种是硬关闭,关闭方直接发送RSET,对方收到后立刻断开连接。#include <unistd.h>
int close(int fd);
成功返回0,错误返回-1。错误码errno:EBADF表示fd不是一个有效描述符;EINTR表示close函数被信号中断;EIO表示一个IO错误。
close函数首先将socket fd的reference减一,若reference依旧大于0,则该socket端口的状态保持不变;若reference等于0,则首先将sender buffer中的数据全部发送出去,并将receive buffer中的数据全部丢弃,最后发送FIN,执行主动关闭。这里的关闭是将读写两个方向的数据传输全部关闭,所以在调用close之后并不能从中读取到数据。
当多个进程共享一个套接字fd时,close每被调用一次,计数减1,直到计数为0时,也就是所有进程都调用了close()后,套接字才会被释放。
windows平台上,对应的函数是closesocket(),分为下面3中情况:
1.close(l_onoff=0 缺省状态):
在套接口上不能再发出发送或接收请求。套接口发送缓冲区中的内容被发送到对端。如果描述字引用计数变为0,在发送完发送缓冲区中的数据后,触发正常的TCP连接终止序列(发送FIN);套接口接收缓冲区中内容被丢弃。
这种方式下,就是在closesocket的时候立刻返回,底层会将未发送完的数据发送完成后再释放资源,也就是优雅的退出。
2.close(l_onoff = 1, l_linger =0):
在套接口上不能再发出发送或接受请求。如果描述子引用计数变为0,RST被发送到对端;连接的状态被置为CLOSED(没有TIME_WAIT状态),套接口发送缓冲区和套接口接受缓冲区的数据被丢弃。
这种方式下,在调用closesocket的时候同样会立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,也就是强制的退出。
3.close(l_onoff =1, l_linger != 0):
在套接口上不能在发出发送或接收请求。套接口发送缓冲区中的内容被发送到对端。如果描述字引用计数变为0,在发送完发送缓冲区中的数据后,触发正常的TCP连接终止序列(发送FIN);套接口接收缓冲区中内容被丢弃。如果在连接变成CLOSED状态前延滞时间到,那么close返回EWOULDBLOCK错误。
这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger得值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。否则,closesocket会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非阻塞型,则closesocket会直接返回值。
8.shutdown()函数
#include <sys/socket.h>how的方式有三种分别是:
int shutdown(int sockfd, int how);
SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。也就是该套接字不再接收数据,任何当前在套接字接收缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接收到的任何数据将被确认然后无声的丢弃掉。但是,进程仍可往套接口发送数据,此方式对套接口发送缓冲区无影响。
SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作。但是,进程仍可以从套接口接收数据,此方式对套接口接收缓冲区无影响。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
返回值:
成功则返回0,错误返回-1,错误码errno:EBADF表示sockfd不是一个有效描述符;ENOTCONN表示sockfd未连接;ENOTSOCK表示sockfd是一个文件描述符而不是socket描述符。
shutdown有2个作用。首先禁止后续的send或者recv,但注意它不会影响底层,也就是说,此前发出的异步send/recv不会返回。其次,在所有发送的包被对方确认后,会发送FIN包给对方,试图优雅的关闭连接。但shutdown本身并不影响任何底层的东西,因此,shutdown并且优雅关闭了连接之后,该socket以及其所关联的资源依然存在,必须调用closesocket才能释放。
shutdown函数禁止了这个套接字的上数据的发送以及接收,并且会发送FIN报文。特别说明的是:shutdown函数并不会去关闭fd,这个函数不会对fd进行操作。
9.close与shutdown区别
1).close - 关闭本进程的socket id,但链接还是开着的,用这个socket id的其它进程还能用这个链接,能读或写这个socket id。
shutdown - 则破坏了socket 链接,读的时候可能侦测到EOF结束符,写的时候可能会收到一个SIGPIPE信号,这个信号可能直到socket buffer被填充了才收到,shutdown还有一个关闭方式的参数,0 不能再读,1不能再写,2 读写都不能。
2).close - 中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。
shutdown - 可直接关闭描述符,不考虑描述符的参考数,可选择中止一个方向的连接。
3).close - 如果多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
shutdown - 如果多个进程共享一个套接字,其中某个进程shutdown(sfd, SHUT_RDWR)后,则其它的进程将无法进行通信。