TCP/IP网络编程(5)
1. 套接字可选项
除了使用套接字的默认参数外,还可自定义设置套接字的多种参数:
协议层 | 选项名 | 读取 | 设置 |
SOL_SOCKET | SO_SNDBUF | O | O |
SO_RCVBUF | O | O | |
SO_REUSEADDR | O | O | |
SO_KEEPALIVE | O | O | |
SO_BROADCAST | O | O | |
SO_DONTROUTE | O | O | |
SO_OOBINLINE | O | O | |
SO_ERROR | O | x | |
SO_TYPE | O | x | |
IPPROTO_IP | IP_TOS | O | O |
IP_TTL | O | O | |
IP_MULTICAST_TTL | O | O | |
IP_MULTICAST_LOOP | O | O | |
IP_MULTICAST_IF | O | O | |
IPPROTP_TCP | TCP_KEEPALIVE | O | O |
TCP_NODELAY | O | O | |
TCP_MAXSEG | O | O |
套接字可选项是分层的,IPPROTO_IP是IP协议相关事项。IPPROTO_TCP层是TCP相关的事项,SOL_SOCKET是套接字相关的通用选项。
上述表格所示的可选项的读取和设置可以通过如下的两个函数来完成:
#include <sys/socket.h> int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
sock: 用于查看选项的套接字文件描述符
level: 要查看的可选项的协议层
optname: 要查看的可选项名称
optval: 保存查看值的缓冲区地址
optlen: 缓冲区optval的大小
返回值:参数optval中保存的可选项信息的字节数
#include <sys/socket.h> int setsockopt(int sock, int level, int optname, const void* optval, socklen_t* optlen);
sock: 用于设置选项的套接字文件描述符
level: 要设置的可选项的协议层
optname: 要设置的可选项名称
optval: 保存设置值的缓冲区地址
optlen: 缓冲区optval的字节数
返回值:成功返回0,失败返回-1
示例代码:查看套接字的类型
// getsockopt.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { WSADATA wsadata; int sockType; // socket类型 int sockTypeLen = sizeof(int); SOCKET tcpSock, udpSock; int sockLen = sizeof(SOCKET); if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { error_handle("Failed to init the winSocket lib"); } tcpSock = socket(PF_INET, SOCK_STREAM, 0); udpSock = socket(PF_INET, SOCK_DGRAM, 0); int res = getsockopt(tcpSock, SOL_SOCKET, SO_TYPE, (char*)&sockType, &sockTypeLen); if (res < 0) { error_handle("Failed to get the tcp socket type!"); } if (sockType == SOCK_STREAM) { printf("The tcp socket type is SOCK_STREAM.\n"); } else if (sockType == SOCK_DGRAM) { printf("The tcp socket type is SOCK_DGRAM.\n"); } int res1 = getsockopt(udpSock, SOL_SOCKET, SO_TYPE, (char*)&sockType, &sockTypeLen); // 获取udp socket的类型 if (res1 < 0) { error_handle("Failed to get the udp socket type!"); } if (sockType == SOCK_STREAM) { printf("The udp socket type is SOCK_STREAM.\n"); } else if (sockType == SOCK_DGRAM) { printf("The udp socket type is SOCK_DGRAM.\n"); } closesocket(tcpSock); closesocket(udpSock); WSACleanup(); return 0; }
运行结果:
设置套接字的输入\输出缓冲区大小
SO_RECVBUF是输入缓冲区大小的相关可选项,SO_SNDBUF是输出缓冲区大小相关的可选项。用这两个选项既可以读取套接字的输入输出缓冲区的大小,又可以对套接字的输入输出缓冲区大小进行设置。
示例代码:
// getsockopt.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { WSADATA wsadata; int snd_buf; int recv_buf; int bufTypeLen = sizeof(int); SOCKET sock; if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { error_handle("Failed to init the winSocket lib"); } sock = socket(PF_INET, SOCK_STREAM, 0); int res = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, &bufTypeLen); int res1 = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recv_buf, &bufTypeLen); if (res < 0 || res1 < 0) { error_handle("Failed to get the tcp socket buf size!"); } printf("The socket send buf is %d bytes.\n", snd_buf); printf("The socket recv buf is %d bytes.\n", recv_buf); closesocket(sock); WSACleanup(); return 0; }
运行结果(可以观察到socket的缓冲区的默认大小):
用户设置且查看socket缓冲区的大小(示例代码):
// getsockopt.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") // error handler void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { WSADATA wsadata; int snd_buf; int recv_buf; // 定义输入输出缓冲区的大小 int sendBufSize = 65535 * 2; int recvBufSize = 65535 * 2; int bufSizeTypeLen = sizeof(sendBufSize); int bufTypeLen = sizeof(int); SOCKET sock; if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { error_handle("Failed to init the winSocket lib"); } sock = socket(PF_INET, SOCK_STREAM, 0); // 获取sock缓冲区的默认大小 int res = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, &bufTypeLen); int res1 = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recv_buf, &bufTypeLen); if (res < 0 || res1 < 0) { error_handle("Failed to get the tcp socket buf size!"); } printf("The socket send buf is %d bytes.\n", snd_buf); printf("The socket recv buf is %d bytes.\n", recv_buf); // 设置socket缓冲区的大小 int state1 = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sendBufSize, bufSizeTypeLen); int state2 = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recvBufSize, bufSizeTypeLen); if (state1 < 0 || state2 < 0) { error_handle("Failed to set the socket buff size!"); } // 读取设置后缓冲区的大小 // 获取sock缓冲区的默认大小 res = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, &bufTypeLen); res1 = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recv_buf, &bufTypeLen); if (res < 0 || res1 < 0) { error_handle("Failed to get the tcp socket buf size!"); } printf("The socket send buf after set is %d bytes.\n", snd_buf); printf("The socket recv buf after set is %d bytes.\n", recv_buf); closesocket(sock); WSACleanup(); return 0; }
运行结果:
注:缓冲区的大小设置需要谨慎处理,因为即使设置了缓冲区的大小,实际的套接字缓冲区大小也不一定会是设置的大小值。仅仅只是通过调用setsockopt函数向系统传递我们的要求。
地址分配错误
在回声服务器的例子中,客户端通过向服务端发送Q,或者在客户端的控制台输入CTRL+C,这两种方式都将终止程序运行,此时客户端会向服务端发送FIN消息,并且经过四次握手之后,断开与服务器端的连接。此时无论是客户端重新连接,或者是重新运行服务器端的程序,也不会有问题。
如果在服务端与客户端已经建立连接的情况下,向服务端控制台输入CTRL+C,即强制关闭服务器。这主要模拟服务器端向客户端发送FIN消息时的情况。如果以这种方式终止程序,那服务器端重新运行的时候将产生问题。如果使用同一端口号重新运行服务器,将输出bind() error消息。在这种情况下,需要等待大概3分钟左右重启服务器,即可重新运行。
上述两种断开连接的方式就是,谁先发送FIN消息给对方。
Time-Wait状态:
在TCP/IP网络编程(2)中,介绍了TCP通过四次握手断开连接的流程。下面对套接字的Time-Wait状态进行介绍。
如下图所示,假设主机A是服务器端,此时图中表示服务器端首先发送FIN消息,断开连接。但是套接字在经过四次握手后并非立即消除,而需要经过一段时间的Time-Wait状态。只有先断开连接(先发送FIN消息)的套接字才会经过Time-Wait状态,因此若服务器端先断开连接,则无法立即重新运行,此时的服务端套接字正处在Time-Wait状态,相应的端口是正在使用的状态。此时如果调用bind()函数就会报错。
注:无论是服务器端还是客户端,在断开连接的时候,先断开的套接字都会经历Time-Wait状态。但是无需考虑客户端的Time-Wait状态,因为客户端的端口在连接的时候是任意指定的,与服务器不同的是,每次程序运行时客户端的端口号都是动态分配的,因此无需过多关注客户端的Time-Wait状态。
Time-Wait状态出现的原因
如上图所示,主机A(服务器)向主机B传输ACK消息(SEQ5001)后立即消除套接字,但是在传输过程中,假设这条消息丢失了而未能传输给主机B,这是主机B会认为自己之前发送的FIN消息(SEQ7501)未能够抵达主机A,从而尝试重传,但此时主机A已经是完全终止的状态,因此主机B永远无法收到主机A最后传来的ACK消息。若此时主机A的套接字处于Time-Wait状态,则会向主机B重传最后的ACK消息,主机B也可以正常终止,基于这样的考虑,先传输FIN消息的主机需要经过Time-Wait状态。
地址再分配
虽然Time-Wait状态很有必要,但是在某些应用场景下会导致问题。例如:假设服务器端发生故障而导致紧急停止,而此时需要尽快重启服务器以提供服务,但是此时服务器因处于Time-Wait状态而需要等待几分钟,因此不太适合这样的应用场景。
解决方案:在套接字可选项中更改SO_REUSEADDR的状态,适当的调整这个参数,就可以将Time-Wait状态下的套接字端口重新分配给新的套接字,SO_REUSEADDR选项的默认值为0,即无法分配Time-Wait状态下的套接字端口,因此需要将此选项的值置为1.
TCP_NODELAY
Nagle算法
Nagle算法诞生于1984年,它应用于TCP层,主要是为了防止因数据过多而发生网络过载。
在使用Nagle算法交换数据的时候,只有收到前一数据的ACK消息时,套接字才会发送下一数据。TCP套接字默认使用Nagle算法交换数据,因此会最大限度的进行缓冲,直到收到ACK。下图对比使用Nagle算法与未使用Nagle算法时数据的传输过程。
如上图所示,假设在开启Nagle算法的条件下,则传输字符串“Nagle”的过程为:开始传输'N'时,在’N‘之前没有传输数据,因此没有需要接收的ACK消息,因此N立即被传输 ,之后开始等待'N'的ACK消息,在等待过程中,后续的'agle'填入缓冲区,在收到'N'的ACK消息后,将'agle'装入数据包进行发送,此时共需要4个数据包完成数据传输。
在闭关Nagle算法传输的时候,假设字符'N' ,'a', 'g', 'l', 'e'依次序传输到缓冲区,因为此时的发送过程已经与ACK消息无关。因此数据达到缓冲区后立刻被发送出去,由图可知,此时共需要10个数据包传输。因此,不开启Nagle算法将对网络流量(traffic:网络负载或混杂程度)产生负面影响。即使仅传输一个字节的数据,其产生的数据包头信息都可能是好几十个字节,为了提高网络的传输效率,需要开启Nagle算法。
注:实际在将数据传输给输出缓冲区的时候,并不是按照逐字进行传输,因此实际的传输情形并非如上图所示,上图只是为了举例说明。
关闭Nagle算法的应用场景:
根据数据传输的特性,在网络流量未收到太大的影响时,关闭Nagle算法可提高数据的传输效率。例如在传输大文件数据的时候,将文件数据传入输出缓冲不会花太多时间,因此,即使不使用Nagle算法,也会在装满输出缓冲区的时候传输数据包,这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大大提高传输效率。一般情况下,不适用Nagle算法可以提高数据的传输效率,但是如果无条件的弃用Nagle算法,就会增加过多的网络流量,反而会影响传输。因此在未准确判断数据特性的情况下不应该禁用Nagle算法。
启停Nagle算法的方法:
// 启动Nagle算法 int opt_val = 1 setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));
查看Nagle算法的弃用效果:
// 查看Nagle算法是否启停 int opt_val; socklen_t opt_len; opt_len = sizeof(opt_val); getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, &opt_len);
// end
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)