文章目录
- [Posix API和网络协议栈,使用TCP实现P2P通信](#Posix API和网络协议栈,使用TCP实现P2P通信)
-
- [1. socket()](#1. socket())
- [2. bind()](#2. bind())
- [3. listen()](#3. listen())
- [4. connect()](#4. connect())
- [5. accept()](#5. accept())
- [6. read()/write(), recv()/send()](#6. read()/write(), recv()/send())
- [7. 内核tcp数据传输](#7. 内核tcp数据传输)
-
- [7.1 TCP流量控制](#7.1 TCP流量控制)
- [7.2 TCP拥塞控制------慢启动/拥塞避免/快速恢复/快速重传](#7.2 TCP拥塞控制——慢启动/拥塞避免/快速恢复/快速重传)
- [8. shutdown()](#8. shutdown())
- [9. close()](#9. close())
-
- [9.1 正常情况](#9.1 正常情况)
- [9.2 主动关闭方在fin_wait_1状态下先收到FIN](#9.2 主动关闭方在fin_wait_1状态下先收到FIN)
- [9.3 双方同时收到FIN报文](#9.3 双方同时收到FIN报文)
- [10. 使用TCP协议实现点对点通信](#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 中的
c
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,则退出循环。
c
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
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
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。