0.libevent学习笔记,从阻塞式socket开始

本文看着这个链接去学的
https://libevent.org/libevent-book/
本文大量借助chatgpt,腾讯混元等网站,难免有错误,如果有问题欢迎提出,初衷仅为本人学习记录使用,我把我碰到的知识尽量记录下来,目前所有程序都是在windows上写的

Windows 上的socket API 和 Linux 的 socket API 非常相似,但并不完全一样。它们都基于 BSD 套接字(Berkeley Sockets)模型,但由于操作系统平台不同,存在一些差异。

功能 Winsock(Windows) BSD/Linux
创建套接字 socket() socket()
绑定地址 bind() bind()
监听连接 listen() listen()
接收连接 accept() accept()
发送数据 send() send()
接收数据 recv() recv()
关闭连接 closesocket() close()

API 名称和参数基本一致,所以很多网络编程代码可以在两个平台上少量修改后通用。

windows上使用socket api通信时需要先初始化

#ifdef _WIN32
	// 存储使用winsock时初始化需要的数据
	WSADATA wsa_data;
	// 调用WSAStartup需要传入Winsock 版本号。
	WSAStartup(0x0201, &wsa_data);
#endif

头文件

功能 Winsock Linux
引入头文件 <winsock2.h>、<ws2tcpip.h> <sys/socket.h>、<netinet/in.h>、<arpa/inet.h>、<unistd.h> 等
链接库 需链接 Ws2_32.lib 不需要额外链接

错误处理

操作 Winsock Linux
错误码 WSAGetLastError() errno
错误码名称 比如 WSAECONNRESET 比如 ECONNRESET

一个简单的阻塞tcp socket客户端程序

struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(80);
inet_pton(AF_INET, "142.250.71.196", &sin.sin_addr);

int fd = socket(AF_INET, SOCK_STREAM, 0);	// 选择tcp传输
if (fd < 0)
{
	std::cerr << "socket";
	return 1;
}
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)))
{
	std::cerr << "connect";
	closesocket(fd);
	return 1;
}

const char query[] = "GET / HTTP/1.0\r\n"
	"Host:www.google.com\r\n"
	"\r\n";
const char* cp = query;
int n_written, remaining = strlen(query);
while (remaining > 0)
{
	n_written = send(fd, cp, remaining, 0);
	if (n_written <= 0)
	{
		std::cerr << "send";
		closesocket(fd);
		return 1;
	}
	remaining -= n_written;
	cp += n_written;
}
char buf[1024];
while (1)
{
	int result = recv(fd, buf, sizeof(buf), 0);
	if (result == 0)
		break;
	else if (result < 0)
	{
		std::cerr << "recv";
		break;
	}
	fwrite(buf, 1, result, stdout);
}

操作系统的原生 ​​Socket API​​ 本身是​​协议无关的​​,既支持 ​​TCP​​ 也支持 ​​UDP​​,具体协议类型由开发者在创建 Socket 时通过参数指定。
udp类型不需要connect,可以直接用sendto,指定ip和端口就可以直接发了

一个专门表示 IPv4 地址和端口号 的结构体变量sockaddr_in,htons的作用是将 主机字节序的端口号 40713 转换为 网络字节序(大端序)

struct sockaddr_in sin;
sin.sin_port = htons(40713);

什么是字节序?​​

​​字节序​​指计算机存储​​多字节数据(如16位/32位整数)​​的顺序,分为两种:

  • ​​小端序(Little-Endian)​​:低位字节在前(常见于x86 CPU)。
  • 例如:40713(十六进制 0x9F09)在内存中存储为 09 9F(低字节 0x09 在前)。
  • ​​大端序(Big-Endian)​​:高位字节在前(网络标准、PowerPC等)。
  • 同一数值存储为 9F 09(高字节 0x9F 在前)

操作系统采用​​小端序(Little-Endian)​​主要是由于历史原因和硬件设计优化,其优势体现在​​数据处理的效率​​和​​硬件设计的简化​​上。(我就不复制粘贴了,反正都是AI告诉我的)

​​TCP 粘包问题
TCP 是​​面向字节流​​的协议,它不保留应用层消息的边界,因此会导致​​粘包(Packet Sticking)​​问题。

​​什么是粘包?​​
​​粘包​​是指发送方多次调用 send() 发送的数据,在接收方的一次 recv() 中全部收到,导致多条消息“粘”在一起,无法区分原始消息边界。

​​示例​​
​​发送方​​:

send(sockfd, "Hello", 5, 0);  // 发送 "Hello"
send(sockfd, "World", 5, 0);  // 发送 "World"

​​接收方​​:

char buf[20];
recv(sockfd, buf, sizeof(buf), 0);  // 可能收到 "HelloWorld"(粘包)

粘包的原因​​

  1. ​​ TCP 是字节流协议​​
  • ​​不维护消息边界​​:TCP 只保证数据按顺序到达,不区分 send() 的调用次数。
  • ​​数据可能合并或拆分​​:
  • ​​Nagle 算法​​:TCP 默认会合并小数据包(减少网络开销)。
  • ​​内核缓冲区机制​​:send() 的数据可能被拆分成多个 TCP 段,或合并成一个段发送。
  1. ​​接收方缓冲区读取方式​​
  • recv() 读取的是​​当前接收缓冲区中的所有可用数据​​,无法自动区分原始消息。

粘包的解决方案

​方法​​ ​​适用场景​​ ​​优点​​ ​​缺点​​
​​固定长度​​ 简单二进制协议 解析快,无需转义 浪费带宽
​​分隔符​​ 文本协议(如HTTP) 灵活,人类可读 需处理转义
​​长度前缀​​ 高效二进制协议 精准控制,无浪费 需预定义最大长度

​​TCP 拆包问题

TCP 拆包(Packet Splitting)是指发送方调用一次 send() 发送的数据,可能被 TCP 协议栈拆分成多个数据包传输,导致接收方需要多次 recv() 才能拼凑出完整消息。

啥是拆包
拆包​​是指一个完整的应用层消息被 TCP 分成多个数据包发送,接收方需要多次读取才能还原原始数据。
示例​​
​​发送方​​:
send(sockfd, "HelloWorld", 10, 0); // 发送 10 字节
​​接收方​​:

char buf[20];
recv(sockfd, buf, 5, 0);  // 第一次收到 "Hello"
recv(sockfd, buf + 5, 5, 0);  // 第二次收到 "World"

posted on 2025-05-24 16:27  sleepy2con  阅读(98)  评论(0)    收藏  举报