文章目录
- Posix API和网络协议栈,使用TCP实现P2P通信
- 1. socket()
- 2. bind()
- 3. listen()
- 4. connect()
- 5. accept()
- 6. read()/write(), recv()/send()
- 7. 内核tcp数据传输
- 7.1 TCP流量控制
- 7.2 TCP拥塞控制——慢启动/拥塞避免/快速恢复/快速重传
- 8. shutdown()
- 9. close()
- 9.1 正常情况
- 9.2 主动关闭方在fin_wait_1状态下先收到FIN
- 9.3 双方同时收到FIN报文
- 10. 使用TCP协议实现点对点通信
- 学习参考
Posix API和网络协议栈,使用TCP实现P2P通信
本文介绍了linux Posix API涉及网络编程的常用函数,并解释其原理和涉及的网络协议栈。最后,利用TCP三次握手中同时打开建立连接的情况实现了P2P通信。
1. socket()
socke()的主要作用是分配fd和建立tcb(tcp control block)。
- 分配fd,内部通过一个bitmap标记一个fd是否已被使用。
- 建立tcb,alloc(),此时还没有分配接受缓冲区和发送缓冲区。
TCB 是操作系统内核中用于跟踪每个 TCP 连接的核心数据结构,包含了与连接相关的状态信息,比如源地址、目标地址、端口号、窗口大小、序列号等。
2. bind()
将本地ip和和端口绑定到fd对应的tcb中。客户端fd如果bind了的话,则本地端口就会固定。
3. listen()
- 将监听套接字tcb中的status设置为TCP_STATUS_LISTEN。
- 为监听套接字tcb分配两个队列:半连接队列syn_queue和全连接队列accept_queue
- syn_queue,存储还未完成三次握手的请求。Linux 中的
tcp_max_syn_backlog
决定了syn_queue
的最大容量。 - accpet_quque,存储已经完成三次握手,等待应用层调用accpet()的请求。如 Linux 中的
somaxconn
决定了其容量。
- syn_queue,存储还未完成三次握手的请求。Linux 中的
int listen(int sockfd, int backlog);
其中backlog最早70年代指的是syn_queue队列的长度,也就是收到SYN但是还没有完成三次握手的请求的队列长度。中间指的是两个队列的长度之和的最大值。现在指的是accpet队列最大长度。这样应用程序可以主要关注待处理连接请求的数量。
tcp连接的生命周期,从什么时候开始?
从收到第一个SYN报文的时候开始,开始创建tcb,连接开始建立。
第三次握手的数据包,如何从半连接队列查找匹配的节点?
每一个TCP报文段中都包含源ip、目的ip、源端口等五元组信息,据此可以查找匹配。
如何解决SYN泛洪/DDOS攻击?
限制半连接队列的最大长度。
4. connect()
connect()用于客户端主动建立与对端的连接,可能有两种情况。
三次握手主要是为了通信的同步,交换序号信息,保证之后的通信不丢失、不乱序。
- 正常主动打开
- 同时打开
适用于P2P通信,需要双方同时调用connect()。
5. accept()
用于从全连接队列中取出一个连接,并分配fd。
水平触发
每次只接受一个连接,效率较低。
边缘触发 + 非阻塞IO
使用一个循环,如果accept返回-1并且errno为EAGAIN或者EWOULDBLOCK,则退出循环。
while (1)
{
fd = accept(listen_fd, &clnt_addr, &clnt_addr_len);
if (fd == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
}
// 处理新连接
}
6. read()/write(), recv()/send()
这些IO函数都是与系统内的接收缓冲区和发送缓冲区之间传输数据,而非直接与网卡。tcp报文段的接受与发送和这些IO函数没有一对一的关系。
7. 内核tcp数据传输
TCP协议头
7.1 TCP流量控制
滑动窗口
TCP使用了滑动窗口中的回退n自动重复请求机制。原理是当某个数据段未被正确接收,则接收方不会确认其后续的数据段;当发送方发现超时或者收到重复的ACK时,会回退到未被确认的第一个数据段继续发送。
延迟确认
如果数据乱序到达,可以等待一段时间,在进行确认。
超时重传
7.2 TCP拥塞控制——慢启动/拥塞避免/快速恢复/快速重传
慢启动是 TCP 在建立连接或从网络拥塞中恢复时使用的拥塞控制算法,旨在探测网络的可用带宽,并逐步增加发送速率,避免在一开始就超载网络。
发送端在收到一个ACK之前,发送窗口的大小是由接收端接收窗口大小和本端拥塞窗口的大小决定的。
慢启动算法是指发送端拥塞窗口在到达慢启动阈值之前(称之为慢启动姐阶段)进行指数级增长。到达ssthresh(slow start threshhold)后进入拥塞避免阶段,拥塞窗口进行线性增长。当超时重传时,ssthresh变为当前拥塞窗口的一半,并重新开始慢启动阶段。
快速恢复用于在丢包被检测到(连续收到三个重复ACK)后,不必将拥塞窗口完全重置为 1 MSS,而是通过调整 cwnd
和 ssthresh
,更快地恢复到稳定传输的状态。
连续收到三个重复ACK,说明这与超时不同,表明网络可能并未完全拥塞,仍然存在一定的可用带宽。
具体来说,当检测到丢包时,发送方将 慢启动阈值 (ssthresh) 设置为当前 cwnd
的一半,然后发送方将 cwnd
减小到 ssthresh
的大小,同时跳过慢启动阶段,直接进入拥塞避免阶段。收到连续三个重复ACK时,也会对数据进行快速重传,而非等到超时。
8. shutdown()
进行半关闭,关闭写端。不推荐使用,因为它会使代码逻辑变复杂。
9. close()
当通信两端有调用close()来关闭tcp连接时,可能会发生以下三种交互情况。
9.1 正常情况
9.2 主动关闭方在fin_wait_1状态下先收到FIN
9.3 双方同时收到FIN报文
10. 使用TCP协议实现点对点通信
参考4. connect()中的同时打开模型。让双方同时(在超时周期之内)调用connect即可。演示代码如下:
client1.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
struct sockaddr_in local_addr = {0}, peer_addr = {0};
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
local_addr.sin_port = htons(2000);
peer_addr.sin_family = AF_INET;
peer_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
peer_addr.sin_port = htons(2001);
if (bind(sock_fd, (struct sockaddr *)&local_addr, sizeof local_addr) == -1)
{
perror("bind()");
close(sock_fd);
return 1;
}
char message[100] = "hello I am KAKA";
while (1)
{
if (connect(sock_fd, (struct sockaddr *)&peer_addr, sizeof peer_addr) == -1)
{
usleep(50);
continue;
}
write(sock_fd, message, strlen(message));
close(sock_fd);
break;
}
return 0;
}
client2.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
struct sockaddr_in local_addr = {0}, peer_addr = {0};
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
local_addr.sin_port = htons(2001);
if (bind(sock_fd, (struct sockaddr *)&local_addr, sizeof local_addr) == -1)
{
perror("bind()");
close(sock_fd);
return 1;
}
peer_addr.sin_family = AF_INET;
peer_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
peer_addr.sin_port = htons(2000);
char buf[100];
int received;
while (1)
{
if (connect(sock_fd, (struct sockaddr *)&peer_addr, sizeof peer_addr) == -1)
{
usleep(50);
continue;
}
if ((received = read(sock_fd, buf, sizeof buf - 1)) != 0)
{
if (received == -1)
{
perror("read()");
close(sock_fd);
return 1;
}
buf[received] = 0;
printf("from peer:\n%s\n", buf);
}
close(sock_fd);
break;
}
return 0;
}
学习参考
学习更多相关知识请参考零声 github。