如何理解之前博客中的服务器和客户端的通信流程,这里以具体函数来解释整个通信过程中的状态转换
三次握手与四次挥手

建立连接

首先服务器通过listen函数进入LISTEN状态
接着客户端通过connect函数发起连接,客户端进入SYN_SENT状态
服务器收到连接请求后,回复一个ACK进入SYN_RCVD状态
客户收到服务器回复的ACK后,对该ACK回复一个ACK,进入ESTABLISHED状态
服务器收到客户端回的ACK后也进入ESTABLISHED状态
三次握手完成,连接建立成功
数据传输

连接建立完成后就可以进行客户端与服务器端的通信
在之前的TCP通信实例中,就是客户端通过write发送数据,服务器端通过read读取数据
断开连接

首先客户端或服务器端任意一方都可以主动使用close函数发起断开连接的请求,进入FIN_WAIT_1状态
另一方收到请求后回复ACK并进入CLOSE_WAIT状态
主动断链方收到ACK后进入FIN_WAIT_2状态,等待被动方处理完成发送断链请求
此时主动断链方进入半关闭状态直至收到被动方的
FIN请求,此时主动断链方仍旧可以接受数据,但是不能发送数据
从程序的角度,可以使用API来控制实现半关闭/半连接状态
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd:需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
被动方处理完断链前的操作后,主动通过close函数发送断链请求,进入LASK_ACK状态
主动断链方收到请求后,回复ACK,进入TIME_WAIT状态,在等待一个固定的时间(两倍报文段寿命,2MSL)后自动进入CLOSE状态
这里等待的原因是为了保证最后这个
ACK丢失后,可以收到被动方的重发请求,重发ACK
被动断链方收到ACK后,进入CLOSE状态
四次挥手完成,连接断开成功
注意:
- 如果有多个进程共享一个套接字,
close每被调用1次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。 - 在多进程中如果一个进程调用了
shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响到其它进程。
数据传输
read和write是linux系统对文件的读取操作,只不过在linux中万物皆文件,因此也可以用于socket通信
但是socket通信还有自己专门的通信函数:recv,send
接受数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()、recvfrom()和recvmsg()调用用于从套接字接收消息。 它们可用于接收无连接套接字和面向连接套接字上的数据。
这三个调用在成功完成后都会返回报文的长度。如果消息太长,无法在提供的缓冲区中容纳,多余的字节可能会被丢弃,这取决于接收消息的套接字类型。
recv() 函数只能用于从已连接的套接字(socket)接收数据。
如果在套接字上没有可用的消息,接收调用将等待消息的到来,除非套接字是非阻塞的,在这种情况下返回值为-1,并且外部变量errno被设置为EAGAIN或EWOULDBLOCK。
接收调用通常立刻返回任何可用的数据,直到达到请求的数量,而不是一直等待收到所请求的全部数据再一次性返回。
recv 参数说明:
sockfd:表示已连接的套接字的文件描述符。buf:指向接收数据的缓冲区的指针。len:表示接收缓冲区的大小。flags:用于指定接收操作的可选标志,如MSG_DONTWAIT、MSG_WAITALL等。
需要注意的是,recv() 函数是一个阻塞函数,如果没有数据可接收,它会一直等待,直到有数据到达或发生错误。如果你希望在接收数据时设置超时或非阻塞模式,可以使用 select() 或 fcntl() 函数来实现。
发送数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
系统调用send()、sendto()和sendmsg()用于向另一个套接字发送消息。
send()调用只有在套接字处于连接状态时才能使用(这样才能知道目标接收者)。 send()和write()的唯一区别是是否有flags参数。 在flags参数为零的情况下,send()等同于write()。 另外,下面的调用
send(sockfd, buf, len, flags)
等价于
sendto(sockfd, buf, len, flags, NULL, 0);
参数sockfd是发送套接字的文件描述符。
如果sendto()在连接模式(SOCK_STREAM, SOCK_SEQPACKET)的套接字上使用,参数dest_addr和addrlen将被忽略(当它们不是NULL和0时,可能返回错误EISCONN)、 当socket没有被连接时,将返回错误ENOTCONN。
否则,目标地址由dest_addr给出,addrlen指定其大小。
对于sendmsg(),目标地址由msg.msg_name给出,msg.msg_namelen指定其大小。
send 参数说明:
sockfd:表示已连接的套接字的文件描述符。buf:指向要发送数据的缓冲区的指针。len:表示要发送的数据的大小。flags:用于指定发送操作的可选标志,如 MSG_DONTWAIT、MSG_NOSIGNAL 等。
send() 函数的返回值是实际发送的字节数,如果返回值为 -1,则表示发生了错误,可以通过 errno 变量获取具体的错误信息。
需要注意的是,send() 函数可能不会立即发送所有的数据,特别是在非阻塞模式下。如果你需要确保所有数据都被发送,可以使用循环来重复调用 send() 直到发送完所有数据。
查看网络信息
netstat 命令是一个用于显示网络状态和统计信息的命令行工具。它可以显示当前打开的网络连接、监听端口、路由表、网络接口等信息,是网络故障排除和性能调优的常用工具之一。
常用的参数:
- -a:显示所有连接和监听端口。
- -n:以数字形式显示地址和端口号,而不是使用主机名和服务名。
- -p:显示与连接关联的进程信息。
- -r:显示路由表。
- -i:显示网络接口信息。
- -s:显示网络统计信息。
下面是一些额外的示例:
显示所有 TCP 连接:
netstat -at
显示所有 UDP 连接:
netstat -au
显示所有监听端口:
netstat -l
显示所有与进程关联的连接:
netstat -p
查找所有使用端口号为 9999 的网络连接,并显示与之关联的进程信息:
netstat -anp| grep 9999
如果有多个进程使用了该端口号,则会显示多行输出。
端口复用
端口复用最常用的用途是:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
注意 端口复用的设置是在服务器调用bind绑定端口之前
参数:
-
sockfd:sockfd必须指向一个打开的套接字描述符 -
level:level指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP)。在设置端口复用时,设置为SOL_SOCKET(端口复用的级别)。 -
optname:用于指定选项的名称,与level一块使用。在设置端口复用时一般用以下两个:SO_REUSEADDR:允许重用本地地址SO_REUSEPORT:允许重用本地端口
-
optval:optval是一个指向某个变量的指针,setsockopt从*optval中取得选项待设置的新值,getsockopt则把已获取的选项当前值存放到*optval中。*optval的大小由最后一个参数指定,它对于setsockopt是一个值参数,对于getsockopt是一个值结果参数。- 在设置端口复用时传进去的是一个int类型的指针,其指向的变量值为1则可以复用,为0则表示不可复用
-
optlen:指定optval的大小
返回值:
- 成功:0
- 失败:-1
使用示例
int opval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opval, sizeof(opval));
下图汇总了可由getsockcpt获取或由setsockopt设置的选项。其中的“数据类型”列给出了指针oprval必须指向的每个选项的数据类型。我们用后跟一对花括号的记法来表示一个结构,如1inger{}就表示struct linger.

