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\
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)通过减少必须发送包的个数来增加网络软件系统的效率。

从上图中可以得到如下结论:
**"只有收到前一数据的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\
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 传递的可选项信息的字节数。