# issue 8 TCP内部原理和UDP编程

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内部原理--四次挥手 ![](https://i-blog.csdnimg.cn/direct/38728e1946214126aa038b92a88ed9cc.png) ## 三、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 的传输速率。** ![](https://i-blog.csdnimg.cn/direct/aad670bb983b440f850c3dc39789e1be.png) 从上图可以看出,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 套接字就可以向任意主机传输数据 ![](https://i-blog.csdnimg.cn/direct/883d50026c4545028f40b8519f3f731e.png) 上图展示了1 个UDP 套接字与2 个不同主机交换数据的过程。也就是说,**只需1 个UDP 套接字就能和多台主机通信。** 创建好TCP 套接字后,传输数据时无需再添加地址信息。因为TCP 套接字将保持与对方 套接字的连接。换言之,TCP 套接字知道目标地址信息。但UDP 套接字不会保持连接状态(UDP 套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前 在信件中填写地址。以下为:填写地址并传输数据时调用的UDP 相关函数。 **发送:** **#include\ 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\ ssize_t recvfrom(int sock, void \*buff,size_t nbytes, int flags,struct sockaddr\*from, socklen_t\*addrlen);** →成功时返回接收的字节数,失败时返回-1。 ●**sock** 用于接收数据的UDP 套接字文件描述符。 ●**buff** 保存接收数据的缓存地址值 ●**nbyte**s 可接收的最大字节数,故无法超过参数buf 所指的缓冲大小。 ●**flags** 可选项参数,若没有则传入0。 ●**from** 存有发送端地址信息的sockaddr 结构体变量的地址值。 ●**addrlen** 保存参数from 的结构体变量长度的变量地址值。 编写UDP 程序时最核心的部分就在于上述两个函数,这也说明二者在UDP 数据传输中 的地位。 注意:UDP是DDOS攻击的一个最主要的形式,因为他是无法拒收的。 ## 五、UDP服务端(下) ```cpp 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:% \n", argv[0]); handle_error("argement is error:"); } ser_sock = socket(PF_INET, SOCK_DGRAM, 0); //UDP if (ser_sock == -1) { handle_error("create socket failed:"); } servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //127.0.0.1 servaddr.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客户端 ```cpp 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 ```cpp 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)通过减少必须发送包的个数来增加网络软件系统的效率。 ![](https://i-blog.csdnimg.cn/direct/c0f3086c8ed54f80b31bfb0fb664a330.png) 从上图中可以得到如下结论: **"只有收到前一数据的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缓存大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解 这些特性并根据实际需要进行更改也十分重要。 ![](https://i-blog.csdnimg.cn/direct/e7252703fd90420ab50f353efd5a6dee.png) 从上表可以看出,套接字可选项是分层的。IPPROTOIP 层可选项是IP 协议相关事项, IPPROTO_TCP 层可选项是TCP 协议相关的事项,SOL_SOCKET 层是套接字相关的通用可选项。 也许有人看到表格会产生畏惧感,但我们真的无需全部背下来或理解,因此不必有负担。实 际能够设置的可选项数量是上表的好几倍,也无需一下子理解所有可选项,实际开发中逐一 掌握即可。接触的可选项多了,自然会掌握大部分重要的。 getsockopt \& setsockopt 我们几乎可以针对上表中的所有可选项进行读取(Get)和设置(Set)(当然,有些可选项 只能进行一种操作)。可选项的读取和设置通过如下2 个函数完成。 **#include\ int getsockopt(int sock, int level,int optname, void \*optval, socklen_t \*optlen);** →成功时返回0,失败时返回-1。 ●**sock** :用于查看选项套接字文件描述符。 ●**l**ev**el** 要查看的可选项的协议层。 ●**optname** 要查看的可选项名。 ●**optval** 保存查看结果的缓冲地址值。 ●**optlen** 向第四个参数optval 传递的缓冲大小。调用函数后,该变量中保存通过第四个参数 返回的可选项信息的字节数。 **#include\ int setsockopt(int sock, int level, int optname, const void\*optval, socklen_t optlen);** →成功时返回0,失败时返回-1。 ●**sock**用于更改可选项的套接字文件描述符。 ●**level** 要更改的可选项协议层。 ●**optname** 要更改的可选项名。 ●**optval** 保存要更改的选项信息的缓冲地址值。 ●**optlen**向第四个参数optval 传递的可选项信息的字节数。

相关推荐
网络抓包与爬虫1 小时前
flutter WEB端启动优化(加载速度,加载动画)
websocket·网络协议·tcp/ip·http·网络安全·https·udp
网络抓包与爬虫1 小时前
从头开发一个Flutter插件(二)高德地图定位插件
websocket·网络协议·tcp/ip·http·网络安全·https·udp
色的归属感2 小时前
一款功能强大的手机使用情况监控工具
websocket·网络协议·tcp/ip·http·网络安全·https·udp
iOS技术狂热者2 小时前
【Android开发基础】手机传感器信息的获取
websocket·网络协议·tcp/ip·http·网络安全·https·udp
Y1nhl2 小时前
Pyspark学习一:概述
数据库·人工智能·深度学习·学习·spark·pyspark·大数据技术
能来帮帮蒟蒻吗2 小时前
Go语言学习(15)结构体标签与反射机制
开发语言·笔记·学习·golang
Aphelios3805 小时前
Java全栈面试宝典:线程机制与Spring IOC容器深度解析
java·开发语言·jvm·学习·rbac
日暮南城故里5 小时前
Java学习------源码解析之StringBuilder
java·开发语言·学习·源码
为你写首诗ge7 小时前
【Unity网络编程知识】FTP学习
网络·unity
安全方案8 小时前
精心整理-2024最新网络安全-信息安全全套资料(学习路线、教程笔记、工具软件、面试文档).zip
笔记·学习·web安全