1、TCP

0、数据协议

七层网络协议

img

1、TCP协议

0、概念

  • TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。

  • 七层协议

    img

  • TCP工作在网络OSI的七层模型中的第四层——传输层,IP在第三层——网络层,ARP 在第二层——数据链路层;数据从应用层发下来,会在每一层都会加上头部信息,进行 封装,然后再发送到数据接收端。

1、IP数据包

  • tcp封装在Ip数据包中

    2、TCP数据报文

  • 各个解析

  • 源端口和目的端口:各个两个字节

  • 序号(seq):4字节,(数据发送的长度,32bit的无符号数,序号到达2^32-1后从0开始。如:300字节,下一个序列号就是 x+300)

  • 确认序号(ack):占4字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号;确认序号应该是上次已成功收到数据字节序号+1。只有ACK标志为1时,确认序号才有效。

  • 数据偏移:4字节,表示数据开始的地方离TCP段的起始处有多远。实际上就是TCP段首部的长度。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位,

  • 窗口:

    • TCP通过滑动窗口的概念来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制。

    • 通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓 冲区。

    • 字节数起始于确认序号字段指明的值(这个值是接收端正期望接收的字节)。窗口大小是一个16bit字段,因而窗口大小最大为65535字节。

3、TCP流量控制

解决的问题: 发送段和接收段速度不同步的问题。

原理:

  • 滑动窗口是接受数据端使用的窗口大小,用来告诉发送端我接收端的缓存区的大小,

  • 数据帧是由编号的,只有发送端窗口的帧才能被发送,只有在接收端窗口的帧才能被接收,

4、TCP阻塞控制

解决的问题:TCP发送方可能因为IP网络的拥塞而被遏制

TCP拥塞控制的几种方法:慢启动拥塞避免,快重传和快恢复

拥塞窗口:发送方维持一个叫做拥塞窗口 cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。

发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就增大一些,但只要网络出现拥塞,拥塞窗口就减小一些。

1、慢启动

TCP在连接过程的三次握手完成后,开始传数据,并不是一开始向网络通道中发送大量的数据包。因为假如网络出现问题,很多这样的大包会积攒在路由器上,很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此TCP协议规定,新建立的连接不能一开始就发送大尺寸的数据包,而只能从一个小尺寸的包开始发送,在发送和数据被对方确认的过程中去计算对方的接收速度,来逐步增加每次发送的数据量(最后到达一个稳定的值,进入高速传输阶段。相应的,慢启动过程中,TCP通道处在低速传输阶段)。

就是说刚建立连接之后,发送窗口是由小增大的,增长是指数增长的

慢启动会导致性能下降

2、拥塞避免
3、快速重传

接收端给发送端的Ack确认只会确认最后一个连续的包

比如:

发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到)此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

1、超时重传

发送端接收不到回传的ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。

解决方法:

  • 一种是仅重传timeout的包。也就是第3份数据。

  • 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。

2、快速重传机制

TCP引入了一种叫Fast Retransmit的算法,如果包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传,而是只是三次相同的ack就重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从#2到#20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

总结: 不管是超时重传还是快速重传确实能保证数据的可靠性,但它无法解决的问题就是:比如发送端发了1、2、3、4、5,而接收端收到了1、3、4、5,那么这个时候它发送的ack是2。那么发送端发送的是重传#2呢还是重传#2,#3,#4,#5的问题。如果在发送#2,#3,#4,#5,本身资源是一种浪费,因为接受方#3,#4,#5已经缓存下来,只需#2,所以在发一遍是无意义的。

5、三次握手与四次挥手

1、TCP三次握手

img

  • 第一次握手:客户端会发送 SYN 报文给服务端,TCP 部首 SYN 标志位置为 1,并随机初始化首部序列号 seq=x;表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN_SENT 状态。

  • 第二次握手:服务端收到客户端的 SYN 报文后,首先,服务端也随机初始化自己的 TCP 部首序列号 seq=y;其次,把首部的确认号填入 ack=x+1;接着,把部首 SYN 和 ACK 标志位都置为 1;最后、把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态。

  • 第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次部首确认号填入 ack=y+1 ,最后把报文发送给服务端。这次报文可以携带客户到服务器的数据。

作用:确认双方的接受和发送能力是否正常、初始化序列号。如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。

三次握手过程中可以携带数据吗?

第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

为什么这样呢?假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理会服务器的接收、发送能力是否正常,然后疯狂得重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,会让服务器更加容易受到攻击。

而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

2、TCP四次挥手

img

  • 第一次挥手:客户端发送 FIN 报文给服务端,TCP 部首 FIN 标志位置为 1,并随机初始化部首序列号 seq=u。之后客户端处于 FIN_WAIT_1 状态。

  • 第二次挥手:服务器收到 FIN 报文后,立即发送一个 ACK 报文,部首确认号为 ack=u+1,序号设为 seq=v。表明已经收到了客户端的报文。之后服务器处于 CLOSE_WAIT 状态。

  • 第三次挥手:如果数据传送完毕,服务器也想断开连接,那么就发送 FIN 报文给客户端,并重新指定一个序号 seq=w,确认号还是ack=u+1,表明可以断开连接。

  • 第四次挥手:客户端收到报文后,发出一个 ACK 报文应答,上一次客户端发送的报文序列号为 seq=u,那么这次序列号就是 seq=u+1,确认号为 ack=w+1。此时客户端处于 TIME_WAIT 状态,需要经过一段时间确保服务器收到自己的应答报文后,才会进入 CLOSED 状态。

3、11种状态名词解析
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RCVD:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
    TIME_WAIT 两个存在的理由:
    1.可靠的实现tcp全双工连接的终止;
    2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)

2、TCP网络编程

0、介绍

0、套接字
# 通用套接字选项(SOL_SOCKET):
SO_RCVTIMEO:设置接收超时时间。影响连接过程中等待服务器响应的时间。
SO_SNDTIMEO:设置发送超时时间。影响连接过程中发送数据的时间。
SO_KEEPALIVE:设置是否启用 TCP keepalive。影响连接在空闲状态下的维持。
SO_REUSEADDR:允许重用本地地址和端口。影响连接后,立即重新使用地址和端口。
SO_LINGER:设置套接字关闭时的行为。影响断开连接时,未发送完毕的数据的处理方式。
# TCP 选项(IPPROTO_TCP):
TCP_NODELAY:禁用 Nagle 算法,即禁用TCP数据发送的合并。影响连接过程中发送数据的效率。
TCP_QUICKACK:允许立即确认接收到的数据。影响连接过程中确认数据的速度。
# IP选项(IPPROTO_IP):
IP_TTL:设置 IP 数据报的生存时间(Time to Live)。影响连接过程中IP数据报的传输。

1、创建套接字
#include <sys/socket.h> // 引入套接字相关的头文件// 创建一个TCP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("Socket creation failed"); // 如果创建失败,打印错误信息
    exit(EXIT_FAILURE); // 退出程序
}

设置套接字选项以调整 TCP 连接超时。对于连接超时(连接建立超时),可以使用 setsockopt 函数设置 SO_RCVTIMEO 和 SO_SNDTIMEO 选项:

if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
    std::cerr << "Error setting socket options" << std::endl;
    close(sockfd);
    return -1;
}struct timeval timeout;
timeout.tv_sec = 10; // 10 seconds
timeout.tv_usec = 0; // 0 microsecondsif (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
    std::cerr << "Error setting socket options" << std::endl;
    close(sockfd);
    return -1;
}
​

2、绑定地址
#include <netinet/in.h> // 引入网络地址相关的头文件
​
struct sockaddr_in serv_addr; // 定义服务器地址结构
serv_addr.sin_family = AF_INET; // 设定地址族为IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的接口
serv_addr.sin_port = htons(PORT); // 绑定到指定端口
​
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("Bind failed"); // 如果绑定失败,打印错误信息
    close(sockfd); // 关闭套接字
    exit(EXIT_FAILURE); // 退出程序
}

3、监听连接请求
if (listen(sockfd, 5) < 0) { // 最大监听队列长度为5
    perror("Listen failed"); // 如果监听失败,打印错误信息
    close(sockfd); // 关闭套接字
    exit(EXIT_FAILURE); // 退出程序
}

4、接受连接
struct sockaddr_in cli_addr; // 定义客户端地址结构
socklen_t clilen = sizeof(cli_addr); // 获取客户端地址结构的大小
​
int connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
if (connfd < 0) {
    perror("Accept failed"); // 如果接受连接失败,打印错误信息
    close(sockfd); // 关闭监听套接字
    exit(EXIT_FAILURE); // 退出程序
}

5、发送数据
#include <unistd.h> // 引入用于文件描述符操作的头文件char buffer[BUFFER_SIZE]; // 定义缓冲区
ssize_t bytes_sent = send(connfd, buffer, strlen(buffer), 0);
if (bytes_sent < 0) {
    perror("Send failed"); // 如果发送失败,打印错误信息
    close(connfd); // 关闭连接套接字
    exit(EXIT_FAILURE); // 退出程序
}

6、接收数据
#include <unistd.h> // 引入用于文件描述符操作的头文件

ssize_t bytes_received = recv(connfd, buffer, sizeof(buffer), 0);
if (bytes_received < 0) {
    perror("Receive failed"); // 如果接收失败,打印错误信息
    close(connfd); // 关闭连接套接字
    exit(EXIT_FAILURE); // 退出程序
}

7、关闭套接字
close(connfd); // 关闭连接套接字
close(sockfd); // 关闭监听套接字

8、setsockopt 函数介绍
int setsockopt(SOCKET s, int level, int optname, const char FAR *optval, int optlen);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level,在套接字级别上(SOL_SOCKET),option_name可以有以下取 值:

1. SO_DEBUG,打开或关闭调试信息。
    当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。
    
2. SO_REUSEADDR,打开或关闭地址复用功能。
    当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为103. SO_DONTROUTE,打开或关闭路由查找功能。
    当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
    
4. SO_BROADCAST,允许或禁止发送广播数据。
    当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
    
5. SO_SNDBUF,设置发送缓冲区的大小。
    发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小 作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
    
6. SO_RCVBUF,设置接收缓冲区的大小。
    接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 27. SO_KEEPALIVE,套接字保活。
    如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。对于所有协议,该操
    作都会根据option_value置或清 sock->sk->sk_flag中的 SOCK_KEEPOPEN位。

8. SO_OOBINLINE,紧急数据放入普通数据流。
    该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
    
9. SO_NO_CHECK,打开或关闭校验和。
    该操作根据option_value的值,设置sock->sk->sk_no_check。
    
10. SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。
    这个值在06之间(包括06),由option_value指定。赋给sock->sk->sk_priority。
    
11. SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。
    该选项的参数(option_value)是一个linger结构:
        struct linger {
            int   l_onoff;   
            int   l_linger;  
        };
    如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
    
12. SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。
    该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。
    
13. SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。
    该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量netstamp_needed加114. SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。
    在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为115. SO_RCVTIMEO,设置接收超时时间。
    该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
    
16. SO_SNDTIMEO,设置发送超时时间。
    该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
  
17. SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。
    该选项最终将设备赋给sock->sk->sk_bound_dev_if。
 
18. SO_ATTACH_FILTER和SO_DETACH_FILTER。
    关于数据包过滤,它们最终会影响sk->sk_filter。

1、TCP服务器

1、保活机制

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200; // 表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
net.ipv4.tcp_keepalive_intvl=75; // 表示每次检测间隔 75 秒;
net.ipv4.tcp_keepalive_probes=9; // 表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过2小时11分15 秒才可以发现一个死亡连接。这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。

如果开启了 TCP 保活机制,需要考虑以下几种情况:

  • 对端程序是正常工作的:当 TCP 保活的探测报文发送给对端,对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

  • 对端程序崩溃并重启:当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

  • 是对端程序崩溃,或对端由于其他原因导致报文不可达:当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该连接已经死亡。

示例

下面是一个简单的TCP服务器示例,它监听客户端的连接请求,并为每个客户端创建一个新的进程来处理数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 3000
#define BUFFER_SIZE 1024

void handle_client(int connfd) {
    char buffer[BUFFER_SIZE];
    char response[BUFFER_SIZE + 10];  // 预留足够空间给 "Echo: "
    ssize_t bytes_received;

    while ((bytes_received = recv(connfd, buffer, BUFFER_SIZE - 1, 0)) > 0) {
        buffer[bytes_received] = '\0'; // 确保字符串以 null 结尾
        printf("Received message: %s\n", buffer);

        // 构造回复信息
        snprintf(response, sizeof(response), "Echo: %s", buffer);

        ssize_t bytes_sent = send(connfd, response, strlen(response), 0);
        if (bytes_sent < 0) {
            perror("Send failed");
            break;
        }
    }

    if (bytes_received < 0) {
        perror("Receive failed");
    }

    close(connfd); // 关闭连接套接字
}

int main() {
    int sockfd, connfd; // 文件描述符
    struct sockaddr_in serv_addr, cli_addr; // 地址结构
    socklen_t clilen; // 地址结构的大小

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的接口
    serv_addr.sin_port = htons(PORT); // 绑定到指定端口

    // 绑定套接字到本地地址
    if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 开始监听连接请求
    if (listen(sockfd, 5) < 0) {
        perror("Listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 循环监听并处理客户端连接
    while (1) {
        clilen = sizeof(cli_addr); // 获取客户端地址结构的大小
        connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen); // 接受客户端连接
        if (connfd < 0) {
            perror("Accept failed");
            continue;
        }

        // 创建子进程处理客户端请求
        pid_t pid = fork();
        if (pid == -1) {
            perror("Fork failed");
            close(connfd);
            continue;
        } else if (pid == 0) {
            // 子进程处理客户端请求
            close(sockfd); // 关闭监听套接字
            handle_client(connfd); // 处理客户端请求
            exit(EXIT_SUCCESS);
        } else {
            // 父进程继续监听新的连接
            close(connfd); // 关闭连接套接字
        }
    }

    // 关闭监听套接字
    close(sockfd);
    return 0;
}

2、TCP客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd; // 文件描述符
    struct sockaddr_in serv_addr; // 地址结构

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT); // 使用指定端口
    inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr); // 将IP地址转换为网络字节序

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 初始化缓冲区
    char buffer[BUFFER_SIZE];
    printf("Enter the message to send: "); // 提示用户输入信息
    fgets(buffer, BUFFER_SIZE, stdin); // 读取用户输入

    // 发送数据到服务器
    ssize_t bytes_sent = send(sockfd, buffer, strlen(buffer), 0);
    if (bytes_sent < 0) {
        perror("Send failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 清空缓冲区
    memset(buffer, 0, BUFFER_SIZE);

    // 接收服务器响应
    ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_received <= 0) {
        perror("Receive failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 确保字符串以null字符结尾
    buffer[bytes_received] = '\0';

    // 打印接收到的信息
    printf("Received message: %s\n", buffer);

    // 关闭套接字
    close(sockfd);
    return 0;
}

推荐内容

2、UDP

1、UDP协议

UDP是一种无连接、不可靠的数据传输协议,其主要特征包括:

  • 无连接性:无需在发送数据之前建立连接,发送方直接向接收方发送数据报。

  • 不可靠性:不提供数据重传、顺序保证或丢失检测,应用层需要自行处理这些问题。

  • 高效性:报文头部简单,开销低,传输速度快。

  • 面向消息:以数据报的形式发送和接收数据,保留了应用层数据的消息边界。

UDP是基于IP的简单协议,不可靠的协议。

UDP的优点:简单,轻量化。

UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题

这里需要注意一点,并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制,所以使用UDP 协议最大的特点就是速度快。

源端口和目的端口,端口号理论上可以有2^16这么多。因为它的长度是16个bit。

Length占用2个字节,标识UDP头的长度,包括首部长度和数据长度。可以有65535字节那么长。但是一般网络在传送的时候,一次一般传送不了那么长的协议(涉及到MTU的问题),就只好对数据分片。

Checksum :校验和,包含UDP头和数据部分。这是一个可选的选项,并不是所有的系统都对UDP数据包加以检验和数据(相对TCP协议的必须来说),但是RFC中标准要求,发送端应该计算检验和。

UDP检验和覆盖UDP协议头和数据,这和IP的检验和是不同的,IP协议的检验和只是覆盖IP数据头,并不覆盖所有的数据。UDP和TCP都包含一个伪首部,这是为了计算检验和而设置的。

发送端的处理流程:
  • 应用程序将数据交给UDP协议栈。

  • 协议栈将数据分割成数据报,添加头部信息。

  • 数据报通过IP协议层传输。

接收端的处理流程:
  • IP层接收到数据报后,根据协议号将其交给UDP层。

  • UDP层验证校验和,移除头部,将数据传递给对应的应用程序。

数据报大小的限制

UDP数据报的大小受到MTU(最大传输单元)的限制。一般情况下,数据报的最大长度为65535字节,其中包括头部和数据部分。然而,过大的数据报可能会在网络传输中被分片,增加了数据丢失的风险。因此,实际应用中,通常将UDP数据报大小控制在一个合理范围内,以平衡效率和可靠性。

1、UDP和ARP之间的交互

当ARP缓存还是空的时候。UDP在被发送之前一定要发送一个ARP请求来获得目的主机的MAC地址,如果这个UDP的数据包足够大,大到IP层一定要对其进行分片的时候,想象中,该UDP数据包的第一个分片会发出一个ARP查询请求,所有的分片都辉等到这个查询完成以后再发送。事实上是这样吗?

结果是,某些系统会让每一个分片都发送一个ARP查询,所有的分片都在等待,但是接受到第一个回应的时候,主机却只发送了最后一个数据片而抛弃了其他,这实在是让人匪夷所思。这样,因为分片的数据不能被及时组装,接受主机将会在一段时间内将永远无法组装的IP数据包抛弃,并且发送组装超时的ICMP报文(其实很多系统不产生这个差错),以保证接受主机自己的接收端缓存不被那些永远得不到组装的分片充满。

2、UDP网络编程

0、流程

1、介绍

1、sendto()函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, 
				int flags, const struct sockaddr *dest_addr, 
				socklen_t addrlen);
  • 功能: 向 to 结构体指针中指定的 ip,发送 UDP 数据,可以发送 0 长度的 UDP 数据包。

  • 参数:

    • sockfd:正在监听端口的套接口文件描述符,通过socket获得;

    • buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据;

    • len:发送缓冲区的大小,单位是字节;

    • flags:填0即可;

    • dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程;

    • addrlen:表示第五个参数所指向内容的长度;

  • 返回值: 成功:返回发送成功的数据长度,失败: -1。

2、recvfrom()函数
#include <sys/types.h>
#include <sys/socket.h>	
ssize_t recvfrom(int sockfd, void *buf, size_t len, 
				int flags, struct sockaddr *src_addr, 
				socklen_t *addrlen);
  • 功能: 接收 UDP 数据,并将源地址信息保存在 from 指向的结构中。

  • 参数:

    • sockfd:正在监听端口的套接口文件描述符,通过socket获得;

    • buf:接收数据缓冲区,往往是使用者定义的数组,该数组装有要发送的数据;

    • len:接收缓冲区的大小,单位是字节;

    • flags:填0即可;

    • src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的;

    • addrlen:表示第五个参数所指向内容的长度;

  • 返回值: 成功:返回接收成功的数据长度,失败: -1。

3、bind()函数
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, 
		socklen_t addrlen);1.2.3.4.
  • 功能: 将本地协议地址与 sockfd 绑定,这样 ip、port 就固定了。

  • 参数:

    • sockfd:正在监听端口的套接口文件描述符,通过socket获得;

    • my_addr:需要绑定的IP和端口;

    • addrlen:my_addr的结构体的大小;

  • 返回值: 成功:0,失败: -1。

4、close()函数
#include <unistd.h>
int close(int fd);1.2.

close函数比较简单,只要填入socket产生的fd即可。

2、UDP服务器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1. 创建通信的套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 通信的套接字和本地的IP与端口绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(3000);    // 大端
    addr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }

    char buf[1024];
    char ipbuf[64];
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    // 3. 通信
    while(1)
    {
        // 接收数据
        memset(buf, 0, sizeof(buf));
        int rlen = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&cliaddr, &len);
        printf("客户端的IP地址: %s, 端口: %d\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
               ntohs(cliaddr.sin_port));
        printf("客户端say: %s\n", buf);

        // 回复数据
        // 数据回复给了发送数据的客户端
        sendto(fd, buf, rlen, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
    }

    close(fd);

    return 0;
}

3、UDP客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1. 创建通信的套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }
    
    // 初始化服务器地址信息
    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(3000);    // 大端
    inet_pton(AF_INET, "172.20.129.244", &seraddr.sin_addr.s_addr);

    char buf[1024];
    char ipbuf[64];
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int num = 0;
    // 2. 通信
    while(1)
    {
        sprintf(buf, "hello, udp %d....\n", num++);
        // 发送数据, 数据发送给了服务器
        sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&seraddr, sizeof(seraddr));

        // 接收数据
        memset(buf, 0, sizeof(buf));
        recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("服务器say: %s\n", buf);
        sleep(1);
    }

    close(fd);

    return 0;
}
点击阅读全文
Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐