1.基础知识


| TCP/IP四层 | OSI七层 | 核心功能 |
|---|---|---|
| 应用层 (消息) | 应用层/表示层/会话层 | 用户接口与数据处理 |
| 传输层(报文端)(数据包) | 传输层 | 端到端可靠传输 |
| 网际层(数据包) | 网络层 | 路由寻址与逻辑分组 |
| 网络接口层 (帧) | 数据链路层/物理层 | 物理介质访问与帧传输 |
2.TCP/IP协议数据单元封装
tcp/ip格式

以太网头
|
目标MAC地址 (6字节) |
源MAC地址 (6字节) |
类型 (2字节) |
载荷(IP数据包,长度可变) |
帧校验序列FCS (4字节 CRC) |
|---|---|---|---|---|
|
(通常不显示) |
IP头

- 版本。占4位。指IP的版本,IPv4数据报中该字段的值是4。
- 首部长度。占4位。以4B为单位,最大可表示的首部长度为60B(15×4B)。最常用的首部长度是20B(5×4B),该字段的值是5,此时不使用任何可选字段。
- 总长度。占16位,指首部和数据之和的长度,单位为字节,因此数据报的最大长度为65535B。以太网帧的最大传送单元(MTU)为1500B,因此当一个IP数据报封装成帧时,数据报的总长度(首部加数据)一定不能超过下面的数据链路层的MTU值。
- 标识。占16位。它是一个计数器,每产生一个数据报就加1,并赋值给标识字段。但它并不是“序号”(因为IP是无连接服务)。当一个数据报的长度超过网络MTU时,必须分片,此时每个数据报片都复制一次标识号,以便能正确地重装成原来的数据报。
- 标志(Flag)。占3位。标志字段的最低位为MF,MF=1表示后面还有分片,MF=0表示最后一个分片。标志字段中间的一位是DF,只有当DF=0时才允许分片。
- 片偏移。占13位。它指出较长的数据报在分片后,某片在原数据报中的相对位置,片偏移以8B为偏移单位。除最后一个分片外,每个分片的长度一定是8B的整数倍。
- 生存时间(TTL)。占8位。数据报在网络中可通过的路由器数的最大值,标识数据报在网络中的寿命,以确保数据报不会永远在网络中循环。路由器在转发数据报前,先将TTL减1。若TTL被减为0,则该数据报必须丢弃。
- 协议。占8位。指出此数据报携带的数据使用何种协议,即数据报的数据部分应上交给哪个协议进行处理,如TCP、UDP等。其中值为6表示TCP,值为17表示UDP。
- 首部检验和。占16位。它只检验数据报的首部,不包括数据部分,不检验数据部分可减少计算的工作量。数据报每经过一个路由器,其首部中的某些字段(如生存时间、总长度、标志、片偏移、源/目的地址)都可能发生变化,因此路由器都要重新计算首部检验和。首部检验和的计算方法与UDP和TCP的检验和的计算方法相同,
- 源地址字段。占4B,标识发送方的IP地址。
- 目的地址字段。占4B,标识接收方的IP地址。
tcp头

- 源端口和目的端口。各占2B。分别表示发送方和接收方使用的端口号。
- 序号。占4B,范围为0~2^32-1,共2^32个序号。TCP连接中传送的字节流中的每个字节都要按顺序编号,序号字段值指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是301,而携带的数据共有100B,表明本报文段的数据的最后一个字节的序号是400,因此下一个报文段的数据序号应从401开始。
- 确认号。占4B,是期望收到对方下一个报文段的第一个数据字节的序号。若确认号为N,则表明到序号N-1为止的所有数据都已正确收到。例如,B正确收到了A发送过来的一个报文段,其序号字段是501,而数据长度是200B(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
- 4)数据偏移(首部长度)。占4位,这里不是IP数据报分片的那个数据偏移,而是表示首部长度(首部中还有长度不确定的选项字段),它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。“数据偏移”的单位是32位(以4B为计算单位)。因为4位二教育进制数能表示的最大值为15,所以TCP首部的最大长度为60B。
- 5)保留。占6位,保留为今后使用,但目前应置为0。
- 6)紧急位URG。当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。紧急数据被插入至报文段数据的最前面,而在紧急数据后面的数据仍是普通数据,因此要与首部中的紧急指针字段配合使用。
- 7)确认位ACK。仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
- 8)推送位PSH(Push)。两个应用进程进行交互式通信时,都希望在键入一个命令后立即就能收到对方的响应,此时发送方TCP把PSH置1,接收方TCP收到PSH=1的报文段后,就尽快交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付。
- 9)复位位RST(Reset)。当RST=1时,表示TCP连接中出现严重差错(如主机崩溃等),必须释放连接,然后重新建立传输连接。此外,它还可用于拒绝一个非法的报文段。
- 10)同步位SYN。当SYN=1时表示这是一个连接请求或连接接受报文。当SYN=1,ACK=0时,表明这是一个连接请求报文,若对方同意建立连接,则应在响应报文中使用SYN=1,ACK=1。关于连接的建立和释放,
- 终止位FIN(Finish)。用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放传输连接。
- 12)窗口。占2B,范围为0~216-1。窗口值告诉对方,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。接收方的数据缓存空间是有限的,因此窗口值作为接收方让发送方设置其发送窗口的依据。例如,设确认号是701,窗口字段是1000。这表明,从701号算起,发送此报文段的一方还有接收1000字节数据(字节序号为701~1700)的接收缓存空间。13)检验和。占2B。检验和字段检验的范围包括首部和数据两部分。计算检验和时,和UDP一样,要在TCP报文段的前面加上12B的伪首部(只需将UDP伪首部的协议字段的17改成6,UDP长度字段改成TCP长度,其他的和UDP一样)。
- 紧急指针。占2B。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据在报文段数据的最前面)。即使窗口为零,也可以发送紧急数据。
- 选项。长度可变,最长可达40B。当不使用选项时,TCP首部长度是20B。TCP最初只规定了一种选项,即最大报文段长度(Maximum Segment Size,MSS),它是TCP报文段中的数据字段的最大长度。之后又陆续增加了窗口扩大、时间戳等选项,具体请参见教材。
- 填充。这是为了使整个首部长度是4B的整数倍。
tcp面向连接的可靠的传输
tcp连接(三次握手)

tcp释放

处于 TIME_WAIT状态的套接字会等待 2MSL(Maximum Segment Lifetime,最大报文段生存时间,通常为 30秒、1分钟或 2分钟)的时间后才会彻底消失。在这段 TIME_WAIT的等待期内,这个套接字(IP地址和端口的组合)仍然被认为是被占用的


tcp编程
转换
-
主机字节序到网络字节序
- u_long htonl(u_long hostlong);
- u_short htons(u_short short);
- 网络字节序到主机字节序
- u_long ntohl(u_long hostlong);
- u_short ntohs(u_short short);
ip地址转换
- int inet_pton(int af, const char* src ,void* dst)
- 将IPV4/IPV6 的地址转换成binary格式
- 使用于IPV4/IPV6
- 能正确处理255.255.255.255的转换问题
- 参数:
- 1.地址协议族(AF_INET或AF_INET6)
- 2.src:是一个指针(填写分点形式的IP地址(主要指IPV4))
- dst:转换的结果给到dst
- inet_ntop(int af,const void *src,char *dst,socklen_t size)
- 把ipv4和ipv6的网络字节序变成本地的字符串形式的IP地址
- 参数
- 1.af:地址协议族(AF_INET或AF_INET6)
- 2.src:是一个指针(32)
- 3.dst:输出结果为32位点分形式的IP地址
- 4.size:长度
socket函数
int socket(int domain,int type,int protocol);
- 参数
- domain
- AF_INET
- AF_INET6
- AF_UNIX,AF_LOCAL
- AF_NETLINK
- AF_PACKET
- type
- SOCK_STREAM: 流式套接字,唯一对应于TCP
- SOCK_DGRAM:数据报套接字,唯一对应着UDP
- SOCK_RAW:原始套接字
- protocol
- 一般填0,原始套接字编程时需填充
- domain
- 返回值
- 成功返回文件描述符
- 出错返回-1
如果是IPV6编程,要使用struct sockddr_in6结构体(man 7 IPV6),通常使用struct sockaddr_storage来编程。
bind函数
//给一个网络套接字(Socket)分配一个具体的地址和端口号
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
listen() 函数详解
1. 函数原型与参数
int listen(int sockfd, int backlog);
- **
sockfd:这是通过socket()函数创建并已用bind()绑定到某个端口的套接字描述符。调用listen()后,这个套接字就从 主动套接字(用于发起连接)转变为 被动套接字**(用于接受连接)。 - **
backlog:这是指已完成三次握手(ESTABLISHED)的连接队列的最大长度**。- 内核维护两个队列:
- 未完成连接队列(SYN_RCVD 状态):存放已收到客户端SYN包、正在等待完成三次握手的连接。
- 已完成连接队列(ESTABLISHED 状态):存放已完成三次握手、等待服务器调用
accept()取走的连接。
- 内核维护两个队列:
accept()函数
//从内核已完成的连接队列中取出一个连接,并为它创建一个新的套接字
服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置套接字选项(避免"Address already in use"错误)
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡
address.sin_port = htons(PORT); // 端口转换为网络字节序
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 4. 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 5. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(address.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("Client connected from %s:%d\n", client_ip, ntohs(address.sin_port));
// 6. 通信循环
while (1) {
// 读取客户端数据
ssize_t bytes_read = read(new_socket, buffer, BUFFER_SIZE);
if (bytes_read < 0) {
perror("read failed");
break;
} else if (bytes_read == 0) {
printf("Client disconnected\n");
break;
}
printf("Received: %s", buffer);
// 回显给客户端
if (write(new_socket, buffer, bytes_read) < 0) {
perror("write failed");
break;
}
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
}
// 7. 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define SERVER_IP "127.0.0.1" // 本地回环地址
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将IP地址从文本转换为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("invalid address/address not supported");
close(sock);
exit(EXIT_FAILURE);
}
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
close(sock);
exit(EXIT_FAILURE);
}
printf("Connected to server at %s:%d\n", SERVER_IP, PORT);
// 4. 通信循环
while (1) {
printf("Enter message (type 'exit' to quit): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 检查退出命令
if (strncmp(buffer, "exit", 4) == 0) {
printf("Exiting...\n");
break;
}
// 发送消息给服务器
if (send(sock, buffer, strlen(buffer), 0) < 0) {
perror("send failed");
break;
}
// 接收服务器回显
ssize_t bytes_received = read(sock, buffer, BUFFER_SIZE);
if (bytes_received < 0) {
perror("read failed");
break;
} else if (bytes_received == 0) {
printf("Server disconnected\n");
break;
}
printf("Server echo: %s", buffer);
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
}
// 5. 关闭连接
close(sock);
return 0;
}
send和write之间的对比
rcv和read之间的对比
都是多了一个flag参数
flag:
0:和read、write一样
udp
实时性低,用在音视频和dns域名解析

对比send和sendto

因为没有没有建立连接,所以需要再发送的时候添加ip和端口
对比recv和recvfrom
同理
bzero
IO多路复用
1.IO模型
- 阻塞I/0:·最常用
- 非阻塞I/0:·可防止进程阻塞在I/0操作上,需要轮询-
- I/0多路复用:·允许同时对多个I/0进行控制
- 信号驱动I/0:·一种异步通信模型
2.阻塞I/O
在写操作时发送阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数 据量的情况下。 这时,写操作不进行任何拷贝工作,将发送阻塞。 一但发送缓冲区内有足够多的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。
3.非阻塞I/O
4.I/0多路复用
1.TCP多客户端通信要创建多线程或多进程的原因:
accept()阻塞导致无法服务新客户:read和accept都是阻塞函数,当read被阻塞时,accept就无法接收新客户- accept会生成多个fd
2.基本常识:linux中每个进程最多可以打开1024个文件,最多有1024个文件描述符,文件描述符的特点:
- 非负整数
- 从最小可用的数字来分配
- 每个进程启动时默认打开0、1、2三个文件描述符
- 多路复用针对不止套接字fd,也针对普通的文件描述fd
3.I/0多路复用的基本思想
把所有的fd放到一个集合里面,监听集合所有的fd是读写事件,如果有就从该fd中读写消息
4.步骤
- 把相关I/O文件描述符加入到集合(fdset)
- 调用select函数去监控,等待集合中文件描述数据(阻塞)
- 当有数据时,select会退出阻塞
- 依次判断那个文件描述符有数据
- 依次处理有数据文件描述符的数据
5.函数

293

被折叠的 条评论
为什么被折叠?



