TCP 通信三大步骤:
1 三次握手建立连接;
2 开始通信,进行数据交换;
3 四次挥手断开连接;
一、TCP内部原理--三次握手
【第一次握手】套接字A∶"你好,套接字B。我这儿有数据要传给你,建立连接吧。"
【第二次握手】套接字B∶"好的,我这边已就绪。"
【第三次握手】套接字A∶"谢谢你受理我的请求。"
首先,请求连接的主机A 向主机B 传递如下信息∶
[SYN] SEQ:1000, ACK: -
该消息中SEQ 为1000,ACK 为空,而SEQ 为1000 的含义如下∶
"现传递的数据包序号为1000,如果接收无误,请通知我向您传递1001 号数据包。"这是首
次请求连接时使用的消息,又称SYN。SYN 是Synchronization 的简写,表示收发数据前传输
的同步消息。
接下来主机B 向A 传递如下消息∶
[SYN+ACK]SEQ:2000, ACK:1001
此时SEQ 为2000,ACK 为1001,而SEQ 为2000 的含义如下∶
"现传递的数据包序号为2000 如果接收无误,请通知我向您传递2001 号数据包。"
而ACK1001 的含义如下∶
"刚才传输的SEQ 为1000 的数据包接收无误,现在请传递SEQ 为1001 的数据包。"
对主机A 首次传输的数据包的确认消息(ACK1001)和为主机B 传输数据做准备的同步消息
(SEQ2000)拥绑发送,因此,此种类型的消息又称SYN+ACK。
收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所做的准备。
通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP
可以保证可靠的数据传输。最后观察主机A 向主机B 传输的消息∶
[ACK]SEQ:1001, ACK:2001
TCP 连接过程中发送数据包时需分配序号。
在之前的序号1000 的基础上加1,也就是分配1001。此时该数据包传递如下消息∶
"已正确收到传输的SEQ 为2000 的数据包,现在可以传输SEQ 为2001 的数据包。"
这样就传输了添加ACK2001 的ACK 消息。至此,主机A 和主机B 确认了彼此均就绪。
特么的,文字太复杂了,来个通俗易懂的。
TCP 三次握手好比在一个夜高风黑的夜晚,你一个人在小区里散步,不远处看见小区里的
一位漂亮妹子迎面而来,但是因为路灯有点暗等原因不能100%确认,所以要通过招手的方
式来确定对方是否认识自己。
你首先向妹子招手(syn),妹子看到你向自己招手后,向你点了点头挤出了一个微笑
(ack)。你看到妹子微笑后确认了妹子成功辨认出了自己(进入established 状态)。
但是妹子有点不好意思,向四周看了一看,有没有可能你是在看别人呢,她也需要确认一下。
妹子也向你招了招手(syn),你看到妹子向自己招手后知道对方是在寻求自己的确认,于是
也点了点头挤出了微笑(ack),妹子看到对方的微笑后确认了你就是在向自己打招呼(进入
established 状态)。
二、TCP内部原理--四次挥手
三、UDP编程--UDP基本原理
在4 层TCP/IP 模型中,第二层传输(Transport)层分为TCP 和UDP 这2 种。数据交换
过程可以分为通过TCP 套接字完成的TCP 方式和通过UDP 套接字完成的UDP 方式。
UDP 套接字的特点:
我们可以通过信件说明UDP 的工作原理,这是讲解UDP 时使用的传统示例,它与UDP
特性完全相符。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒
即可。当然,信件的特点使我们无法确认对方是否收到。另外,邮寄过程中也可能发生信件
丢失的情况。也就是说,信件是一种不可靠的传输方式。与之类似,UDP 提供的同样是不可
靠的数据传输服务。
"既然如此,TCP 应该是更优质的协议吧?"
如果只考虑可靠性,TCP 的确比UDP 好。但UDP 在结构上比TCP 更简洁。UDP 不会发
送类似ACK 的应答消息,也不会像SEQ 那样给数据包分配序号。因此,UDP 的性能有时比
TCP 高出很多。编程中实现UDP 也比TCP 简单。另外,UDP 的可靠性虽比不上TCP,但也不
会像想象中那么频繁地发生数据损毁。因此,在更重视性能而非可靠性的情况下,UDP 是一
种很好的选择。
既然如此,UDP 的作用到底是什么呢?为了提供可靠的数据传输服务,TCP 在不可靠的
IP 层进行流控制,而UDP 就缺少这种流控制机制。
流控制是区分UDP 和TCP 的最重要的标志。但若从TCP 中除去流控制,所剩内容也屈
指可数。也就是说,TCP 的生命在于流控制。
如果把TCP 比喻为电话,把UDP 比喻为信件。但这只是形容协议工作方式,并没有包
含数据交换速率。请不要误认为"电话的速度比信件快,因此TCP 的数据收发速率也比UDP
快"。实际上正好相反。TCP 的速度无法超过UDP,但在收发某些类型的数据时有可能接近
UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近UDP 的传输速率。
从上图可以看出,IP 的作用就是让离开主机B 的UDP 数据包准确传递到主机A。但把
UDP 包最终交给主机A 的某一UDP 套接字的过程则是由UDP 完成的。UDP 最重要的作用就
是根据端口号将传到主机的数据包交付给最终的UDP 套接字。
其实在实际的应用场景中,UDP 也具有一定的可靠性。网络传输特性导致信息丢失频发,
可若要传递压缩文件(发送1 万个数据包时,只要丢失1 个就会产生问题),则必须使用
TCP,因为压缩文件只要丢失一部分就很难解压。但 通过网络实时传输视频或音频时的情况
有所不同。对于多媒体数据而言,丢失一部分也没有太大问题,这只会引起短暂的画面抖动,
或出现细微的杂音。但因为需要提供实时服务,速度就成为非常重要的因素,此时需要考虑
使用UDP。但UDP 并非每次都快于TCP,TCP 比UDP 慢的原因通常有以下两点。
1 收发数据前后进行的连接设置及清除过程。
2 收发数据过程中为保证可靠性而添加的流控制。
尤其是收发的数据量小但需要频繁连接时,UDP 比TCP 更高效。
四、UDP服务端(上)
UDP 中的服务器端和客户端没有连接
UDP 服务器端/客户端不像TCP 那样在连接状态下交换数据,因此与TCP 不同,无需经
过连接过程。也就是说,不必调用TCP 连接过程中调用的listen 函数和accept 函数。UDP 中
只有创建套接字的过程和数据交换过程。
UDP 服务器端和客户端均只需1 个套接字
TCP 中,套接字之间应该是一对一的关系。若要向10 个客户端提供服务,则除了守门
的服务器套接字外,还需要10 个服务器端套接字。但在UDP 中,不管是服务器端还是客户
端都只需要1 个套接字。之前解释UDP 原理时举了信件的例子,收发信件时使用的邮筒可
以比喻为UDP 套接字。只要附近有1 个邮筒,就可以通过它向任意地址寄出信件。同样,
只需1 个UDP 套接字就可以向任意主机传输数据
上图展示了1 个UDP 套接字与2 个不同主机交换数据的过程。也就是说,只需1 个UDP
套接字就能和多台主机通信。
创建好TCP 套接字后,传输数据时无需再添加地址信息。因为TCP 套接字将保持与对方
套接字的连接。换言之,TCP 套接字知道目标地址信息。但UDP 套接字不会保持连接状态(UDP
套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前
在信件中填写地址。以下为:填写地址并传输数据时调用的UDP 相关函数。
发送:
#include<sys/socket.h>
ssize_t sendto(int sock,void*buff,size_t nbytes,int flags,struct sockaddr *to, socklen_t addrlen);
→成功时返回传输的字节数,失败时返回-1。
● sock
用于传输数据的UDP 套接字文件描述符。
● buff
保存待传输数据的缓冲地址值。
● nbytes
待传输的数据长度,以字节为单位。
● flags
可选项参数,若没有则传递0。
● to
存有目标地址信息的sockaddr 结构体变量的地址值。
● addrlen
传递给参数to 的地址值结构体变量长度。
上述函数与之前的TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接
下来介绍接收UDP 数据的函数。UDP 数据的发送端并不固定,因此该函数定义为可接收发
送端信息的形式,也就是将同时返回UDP 数据包中的发送端信息。
接收:
#include<sys/socket.h>
ssize_t recvfrom(int sock, void *buff,size_t nbytes, int flags,struct sockaddr*from, socklen_t*addrlen);
→成功时返回接收的字节数,失败时返回-1。
●sock 用于接收数据的UDP 套接字文件描述符。
●buff 保存接收数据的缓存地址值
●nbytes 可接收的最大字节数,故无法超过参数buf 所指的缓冲大小。
●flags 可选项参数,若没有则传入0。
●from 存有发送端地址信息的sockaddr 结构体变量的地址值。
●addrlen 保存参数from 的结构体变量长度的变量地址值。
编写UDP 程序时最核心的部分就在于上述两个函数,这也说明二者在UDP 数据传输中
的地位。
注意:UDP是DDOS攻击的一个最主要的形式,因为他是无法拒收的。
五、UDP服务端(下)
int lession73(int argc, char* argv[])
{printf("server start");system("echo 'server start' > /output.txt");int ser_sock = -1;char message[1024] = "";struct sockaddr_in servaddr, clientaddr;socklen_t clientlen = 0;if (argc != 2) {printf("usage:% <port>\n", argv[0]);handle_error("argement is error:");}ser_sock = socket(PF_INET, SOCK_DGRAM, 0); //UDPif (ser_sock == -1) {handle_error("create socket failed:");}servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //127.0.0.1servaddr.sin_port = htons((short)atoi(argv[1]));if (bind(ser_sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {handle_error("bind failed:");}for (int i = 0; i < 50; i++) {system("echo 'server start 0' > /output.txt");clientlen = sizeof(clientaddr);ssize_t len = recvfrom(ser_sock, message, sizeof(message), 0, (struct sockaddr*)&clientaddr, &clientlen);system("echo 'server start 2' > /output.txt");sendto(ser_sock, message, len, 0, (struct sockaddr*)&clientaddr, clientlen);}printf("server ok");close(ser_sock);return 0;
}
六、UDP客户端
int lession74(int argc, char* argv[]) {int client_sock;struct sockaddr_in serv_addr;socklen_t serv_len = sizeof(serv_addr);char massege[1024] = "";//校验参数if (argc != 3) {printf("usge:%s ip port\n", argv[0]);handle_error("argement error!");}client_sock = socket(AF_INET, SOCK_DGRAM, 0);if (client_sock == -1) {handle_error("soket create failed!");}memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = inet_addr(argv[1]);serv_addr.sin_port = htons((short)atoi(argv[2]));while (1) {printf("input massege(Q to quit):");scanf("%s", massege);if ((strcmp(massege, "Q") == 0) || (strcmp(massege, "q") == 0)) {break;}printf("Sending message: '%s' with length: %zu\n", massege, strlen(massege));//printf("Sending message: '%s' with length: %zu\n", massege, strlen(massege));printf("debug:%s", massege);ssize_t len=sendto(client_sock, massege, strlen(massege), 0, (sockaddr*)&serv_addr, serv_len);memset(massege, 0, (unsigned int)len);recvfrom(client_sock, massege, sizeof(massege), 0, (sockaddr*)&serv_addr, &serv_len);printf("recv:%s\n", massege);}close(client_sock);return 0;}
七、UDP的传输特性和调用
前面讲解了UDP 服务器端/客户端的实现方法。但如果仔细观察UDP 客户端会发现,它
缺少把IP 和端口分配给套接字的过程。TCP 客户端调用connect 函数自动完成此过程,而
UDP 中连能承担相同功能的函数调用语句都没有。究竟在何时分配IP 和端口号呢?
UDP 程序中,调用sendto 函数传输数据前应完成对套接字的地址分配工作,因此调用bind
函数。当然,bind 函数在TCP 程序中出现过,但bind 函数不区分TCP 和UDP,也就是说,
在UDP 程序中同样可以调用。另外,如果调用sendto 函数时发现尚未分配地址信息,则在
首次调用sendto 函数时给相应套接字自动分配IP 和端口。而且此时分配的地址一直保留到
程序结束为止,因此也可用来与其他UDP 套接字进行数据交换。当然,IP 用主机IP,端口
号选尚未使用的任意端口号。
综上所述,调用sendto 函数时自动分配IP 和端口号,因此,UDP 客户端中通常无需额
外的地址分配过程。
八、SO_REUSEADDR
void client78() {int client = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr)); //清零 防止意外servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");servaddr.sin_port = htons(33005);int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));while (ret == 0) {printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);char buffer[256] = "";fputs("input message(Q to quit):", stdout);//输出提示符fgets(buffer, sizeof(buffer), stdin/*标准输入流*/);//读入一行if ((strcmp(buffer, "Q\n") == 0) || (strcmp(buffer, "q\n") == 0)) {break;}size_t len = strlen(buffer);size_t send_len = 0;while (send_len < len) {ssize_t ret = write(client, buffer + send_len, len - send_len);//发给服务器if (ret <= 0){fputs("write failed!\n", stdout);//close(client);std::cout << "client done!" << std::endl;return;}send_len += (size_t)ret;}memset(buffer, 0, sizeof(buffer));size_t read_len = 0;while (read_len < len){ssize_t ret = read(client, buffer + read_len, len - read_len);if (ret <= 0){fputs("read failed!\n", stdout);//close(client);std::cout << "client done!" << std::endl;return;}send_len += (size_t)ret;}std::cout << "from server:" << buffer;}//close(client);std::cout << "client done!" << std::endl;
}
void server78() {printf("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);int sock,client,optval=0;struct sockaddr_in addr,cli;socklen_t addrlen=sizeof(addr);char message[256] = "";sock = socket(PF_INET, SOCK_STREAM, 0);getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);printf("SO_REUSEADDR=%d\n", optval);optval = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);printf("SO_REUSEADDR=%d\n", optval);memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr("127.0.0.1");addr.sin_port = htons(33005);addrlen = sizeof(addr);if (bind(sock, (struct sockaddr*)&addr, addrlen) == -1) {handle_error("bind failed!");}listen(sock, 3);client = accept(sock, (struct sockaddr*)&cli, &addrlen);read(client, message, sizeof(message));close(client);close(sock);return;
}
void lession78(char*option){if (strcmp(option, "1") == 0) {//服务器server78();server78();server78();server78();server78();server78();server78();}else {//客户端client78();}}
九、TCP_NODELAY
"什么是Nagle 算法?使用该算法能够获得哪些数据通信特性?"
Nagle 算法是以他的发明人John Nagle 的名字命名的,它用于自动连接许多的小缓冲
器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。
从上图中可以得到如下结论:
"只有收到前一数据的ACK 消息时,Nagle 算法才发送下一数据。"
TCP 套接字默认使用Nagle 算法交换数据,因此最大限度地进行缓冲,直到收到ACK。
上图左侧正是这种情况。为了发送字符串"Nagle",将其传递到输出缓冲。这时头字符"N"之
前没有其他数据(没有需接收的ACK),因此立即传输。之后开始等待字符"N"的ACK 消息,
等待过程中,剩下的"agle"填入输出缓冲。接下来,收到字符"N"的ACK 消息后,将输出缓冲
的"agle"装入一个数据包发送。也就是说,共需传递4 个数据包以传输1 个字符串。
接下来分析未使用Nagle 算法时发送字符串"Nagle"的过程。假设字符"N"到"e"依序传到
输出缓冲。此时的发送过程与ACK 接收与否无关,因此数据到达输出缓冲后将立即被发送
出去。从上图右侧可以看到,发送字符串"Nagle"时共需10 个数据包。由此可知,不使用Nagle
算法将对网络流量产生负面影响。即使只传输1 个字节的数据,其头信息都有可能是几十个
字节。因此,为了提高网络传输效率,必须使用Nagle 算法。
在程序中将字符串传给输出缓冲时并不是逐字传递的,故发送字符串"Nagle"的实际情
况并非如上图所示。但如果隔一段时间再把构成字符串的字符传到输出缓冲(如果存在此
类数据传递)的话,则有可能产生类似上图的情况。上图中就是隔一段时间向输出缓冲传递
待发送数据的。
但Nagle 算法并不是什么时候都适用。根据传输数据的特性,网络流量未受太大影响时,
不使用Nagle 算法要比使用它时传输速度快。最典型的是"传输大文件数据"。将文件数据传
入输出缓冲不会花太多时间,因此,即便不使用Nagle 算法,也会在装满输出缓冲时传输数
据包。这不仅不会增加数据包的数量,反而会在无需等待ACK 的前提下连续传输,因此可
以大大提高传输速度。
一般情况下,不适用Nagle 算法可以提高传输速度。但如果无条件放弃使用Nagle 算法,
就会增加过多的网络流量,反而会影响传输。因此,未准确判断数据特性时不应禁用Nagle
算法。
刚才说过的"大文件数据"应禁用Nagle 算法。换言之,如果有必要,就应禁用Nagle 算
法。"Nagle 算法使用与否在网络流量上差别不大,使用Nagle 算法的传输速度更慢"禁用方
法非常简单。
十、IO缓存大小
我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解
这些特性并根据实际需要进行更改也十分重要。
从上表可以看出,套接字可选项是分层的。IPPROTOIP 层可选项是IP 协议相关事项,
IPPROTO_TCP 层可选项是TCP 协议相关的事项,SOL_SOCKET 层是套接字相关的通用可选项。
也许有人看到表格会产生畏惧感,但我们真的无需全部背下来或理解,因此不必有负担。实
际能够设置的可选项数量是上表的好几倍,也无需一下子理解所有可选项,实际开发中逐一
掌握即可。接触的可选项多了,自然会掌握大部分重要的。
getsockopt & setsockopt
我们几乎可以针对上表中的所有可选项进行读取(Get)和设置(Set)(当然,有些可选项
只能进行一种操作)。可选项的读取和设置通过如下2 个函数完成。
#include<sys/socket.h>
int getsockopt(int sock, int level,int optname, void *optval, socklen_t *optlen);
→成功时返回0,失败时返回-1。
●sock :用于查看选项套接字文件描述符。
●level 要查看的可选项的协议层。
●optname 要查看的可选项名。
●optval 保存查看结果的缓冲地址值。
●optlen 向第四个参数optval 传递的缓冲大小。调用函数后,该变量中保存通过第四个参数
返回的可选项信息的字节数。
#include<sys/socket.h>
int setsockopt(int sock, int level, int optname, const void*optval, socklen_t optlen);
→成功时返回0,失败时返回-1。
●sock 用于更改可选项的套接字文件描述符。
●level 要更改的可选项协议层。
●optname 要更改的可选项名。
●optval 保存要更改的选项信息的缓冲地址值。
●optlen 向第四个参数optval 传递的可选项信息的字节数。