linux socket编程:shutdown、recv、send、sendfile、getsockname、getpeername等函数和netstat命令、TIME_WAIT 状态详细介绍
1.1 流式套接字上的部分读和部分写
-
套接字上可用的数据比实际请求的数据少
-
传输过程中系统调用被信号处理打断
-
套接字使用非阻塞模式,可能只传输了一部分
-
传输未完成产生一个异步错误,例如一方连接关闭
-
解决办法:
-
全部读函数:
/* Read 'n' bytes from 'fd' into 'buf', restarting after partial reads or interruptions by a signal handlers */ ssize_t readn(int fd, void *buffer, size_t n) { ssize_t numRead; /* # of bytes fetched by last read() */ size_t totRead; /* Total # of bytes read so far */ char *buf; buf = buffer; /* No pointer arithmetic on "void *" */ for (totRead = 0; totRead < n; ) { numRead = read(fd, buf, n - totRead); if (numRead == 0) /* EOF */ return totRead; /* May be 0 if this is first read() */ if (numRead == -1) { if (errno == EINTR) continue; /* Interrupted --> restart read() */ else return -1; /* Some other error */ } totRead += numRead; buf += numRead; } return totRead; /* Must be 'n' bytes if we get here */ }
-
-
全部写函数
/* Write 'n' bytes to 'fd' from 'buf', restarting after partial write or interruptions by a signal handlers */ ssize_t writen(int fd, const void *buffer, size_t n) { ssize_t numWritten; /* # of bytes written by last write() */ size_t totWritten; /* Total # of bytes written so far */ const char *buf; buf = buffer; /* No pointer arithmetic on "void *" */ for (totWritten = 0; totWritten < n; ) { numWritten = write(fd, buf, n - totWritten); /* The "write() returns 0" case should never happen, but the following ensures that we don't loop forever if it does */ if (numWritten <= 0) { if (numWritten == -1 && errno == EINTR) continue; /* Interrupted --> restart write() */ else return -1; /* Some other error */ } totWritten += numWritten; buf += numWritten; } return totWritten; /* Must be 'n' bytes if we get here */ }
1.2 shutdown()系统调用
#include <sys/socket.h>
/************************
函数功能:用来关闭双向通信通道的端口
int shutdown(int sockfd, int how);
On success 0 will return,else -1,errno will be set
/*
sockfd:指定要操作套接字文件描述符
how:
SHUT_RD:关闭通信一端的读端口,在改方可以继续写,但是不能读取数据,尝试读取的时候会返回文件结尾,对端会受到SIGPIPE信号,尝试写的话会产生EPIPE错误
SHUT_WR:关闭通信一端的写端,尝试本地写操作会产生SIGPIPE信号和EPIPE错误
SHUT_RDWR:关闭一方的读端口和写端口
*/
shutdown()和close()
的区别:- shutdown是根据文件描述符来操作,和文件描述符无关,调用shutdown之后,还是要调用close
- close是关闭通信双方,并且是对文件描述符操作,如果多个文件描述指向同一个文件,用close关闭其中一个文件描述符,另外一个文件描述符也可以继续通信
1.3 recv()和send():专用于套接字的I/O系统调用
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*
1. send解析
sockfd:指定发送端套接字描述符。
buff: 存放要发送数据的缓冲区
nbytes: 实际要改善的数据的字节数
flags: 一般设置为0
1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR;
2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes
3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完
4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。
5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
6) send函数把buff中的数据成功copy到sockfd的改善缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send的socket函数在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传递完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)
7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*
2.recv函数
sockfd: 接收端套接字描述符
buff: 用来存放recv函数接收到的数据的缓冲区
nbytes: 指明buff的长度
flags: 一般置为0
1) recv先等待s的发送缓冲区的数据被协议传送完毕,如果协议在传送sock的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
2) 如果套接字sockfd的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一起等待,直到把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据copy到buff中(注意协议接收到的数据可能大于buff的长度,所以在这种情况下要调用几次recv函数才能把sockfd的接收缓冲区中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)
3) recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR。如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
4) 在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用 recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
*/
-
recv和send前3个参数等同于write和read
-
flag参数:
1.4 sendfile()系统调用
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
/*
in_fd:打开的文件的文件描述符
out_fd:套接字描述符
offset:文件的偏移量
count:请求发送的字节数
*/
- 和write或者send的区别:
1.5 获取套接字地址
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*对于这两个函数,如果函数调用成功,则返回0,如果调用出错,则返回-1。
使用这两个函数,我们可以通过套接字描述符来获取自己的IP地址和连接对端的IP地址,如在未调用bind函数的TCP客户端程序上,可以通过调用getsockname()函数获取由内核赋予该连接的本地IP地址和本地端口号,还可以在TCP的服务器端accept成功(建立连接)后,通过getpeername()函数来获取当前连接的客户端的IP地址和端口号*/
1.6 深入探究TCP协议
-
TCP报文格式:
- 源端口号:TCP发送端的端口号
- 目的端口号:TCP接收端的端口号
- 序列号:标识了TCP发送数据的字节流,表示在报文段中的第一个数据字上
- 确认序列号:如果设置了控制位当中的ACK位,这个字段包含了接收方期望从发送方接收到下一个数据字节的序列号
- 控制位:8bit组成,每一位指定了报文的含义,其中重要的几位如下:
- ACK位:该位如果设置了,确认序号字段包含的信息有效
- SYN位:同步序列号,建立连接时,双方需要交换设置了该位置的报文,这样可以使得TCP连接的两端可以指定初始序列号,用于数据的双向传输
- FIN位:发送端提示已经完成了发送任务
- 窗口大小:该字段用在接收端发送ACK确认时提示自己可接受的数据空间大小
- 数据:包含传输的用户数据,如果报文没有数据的话,该字段为0(比如一个简单的ACK报文)
1.6.2 TCP序列号和确认机制
-
TCP给每一个数据分配了一个逻辑序列号,其值为数据域第一个字节的逻辑偏移
-
TCP采用主动确认的方式,当一个TCP接收端成功接收数据之后,TCP接收端会发送一个确认消息(设置了ACK位的报文段)给TCP发送端,并且该消息的确认序号字段被设置为接收方所希望接收的下一个数据字节的逻辑序列号,一般是上一个收到的数据字节的序列号加1
-
TCP发送端口发送报文时会设置一个定时器,如果在定时器超前之前没有接收到确认报文,那么该报文会重新发送
-
TCP应答机制:
5. 箭头的倾斜程度表示报文所需的时间
1.6.3 TCP协议状态机以及状态迁移图
1.6.4 TCP连接的建立
- TCP连接建立时的三次握手:
1.6.5 TCP连接的终止:
1.6.6 TIME_WAIT 状态
- 存在的主要目的:
- 实现可靠的连接终止
- 让老的重复的报文段在网络中失效,在建立新的连接时候不在需要它们
- 问题:如果有一个处于TIME_WAIT状态的地址上时,会导致EAADRINUSE的错误(地址已使用)
1.7 监视套接字:netstat
-
作用:显示系统中internet和unix域的套接字状态:
-
选项:
-
netstat -a --inet -p:
- Proto:套接字所使用的协议
- Recv-Q:套接字接受缓冲区中还未被本地应用程序接受的数据的字节数
- Send-Q:表示套接字发送缓冲区中排队等待发送的字节数
- Local Address:套接字所绑定的地址,以主机IP:端口形式表示,默认情况下以名称来显示,主机中*表示绑定的是通配地址
- Foreign Address:对端套接字所绑定的地址。字符串包含两个‘*’号表示没有对端地址
- Stade:套接字所处的状态
1.8 使用tcpdump来监视TCP流量
1.9 SO_REUSEADDR套接字选项
-
主要用来解决服务器重新绑定的时候出现地址已经使用的问题
-
而客户端一般不会出现这种情况,因为客户端一般绑定的是临时客户端
-
解决办法:
61.11 在accept()中继承标记和选项
-
accept()返回的套接字不会的继承监听套接字的属性: