0.libevent学习笔记,从阻塞式socket开始
<p>本文看着这个链接去学的<br>https://libevent.org/libevent-book/<br>
本文大量借助chatgpt,腾讯混元等网站,难免有错误,如果有问题欢迎提出,初衷仅为本人学习记录使用,我把我碰到的知识尽量记录下来,目前所有程序都是在windows上写的</p>
<p>Windows 上的socket API 和 Linux 的 socket API 非常相似,但并不完全一样。它们都基于 BSD 套接字(Berkeley Sockets)模型,但由于操作系统平台不同,存在一些差异。</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>Winsock(Windows)</th>
<th>BSD/Linux</th>
</tr>
</thead>
<tbody>
<tr>
<td>创建套接字</td>
<td>socket()</td>
<td>socket()</td>
</tr>
<tr>
<td>绑定地址</td>
<td>bind()</td>
<td>bind()</td>
</tr>
<tr>
<td>监听连接</td>
<td>listen()</td>
<td>listen()</td>
</tr>
<tr>
<td>接收连接</td>
<td>accept()</td>
<td>accept()</td>
</tr>
<tr>
<td>发送数据</td>
<td>send()</td>
<td>send()</td>
</tr>
<tr>
<td>接收数据</td>
<td>recv()</td>
<td>recv()</td>
</tr>
<tr>
<td>关闭连接</td>
<td>closesocket()</td>
<td>close()</td>
</tr>
</tbody>
</table>
<p>API 名称和参数基本一致,所以很多网络编程代码可以在两个平台上少量修改后通用。</p>
<p>windows上使用socket api通信时需要先初始化</p>
<pre><code>#ifdef _WIN32
// 存储使用winsock时初始化需要的数据
WSADATA wsa_data;
// 调用WSAStartup需要传入Winsock 版本号。
WSAStartup(0x0201, &wsa_data);
#endif
</code></pre>
<p><strong>头文件</strong></p>
<table>
<thead>
<tr>
<th>功能</th>
<th>Winsock</th>
<th>Linux</th>
</tr>
</thead>
<tbody>
<tr>
<td>引入头文件</td>
<td><winsock2.h>、<ws2tcpip.h></td>
<td><sys/socket.h>、<netinet/in.h>、<arpa/inet.h>、<unistd.h> 等</td>
</tr>
<tr>
<td>链接库</td>
<td>需链接 Ws2_32.lib</td>
<td>不需要额外链接</td>
</tr>
</tbody>
</table>
<p><strong>错误处理</strong></p>
<table>
<thead>
<tr>
<th>操作</th>
<th>Winsock</th>
<th>Linux</th>
</tr>
</thead>
<tbody>
<tr>
<td>错误码</td>
<td>WSAGetLastError()</td>
<td>errno</td>
</tr>
<tr>
<td>错误码名称</td>
<td>比如 WSAECONNRESET</td>
<td>比如 ECONNRESET</td>
</tr>
</tbody>
</table>
<p>一个简单的阻塞tcp socket客户端程序</p>
<pre><code>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;
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);
}
</code></pre>
<p>操作系统的原生 Socket API 本身是协议无关的,既支持 TCP 也支持 UDP,具体协议类型由开发者在创建 Socket 时通过参数指定。<br>
udp类型不需要connect,可以直接用sendto,指定ip和端口就可以直接发了</p>
<p>一个专门表示 IPv4 地址和端口号 的结构体变量sockaddr_in,htons的作用是将 主机字节序的端口号 40713 转换为 网络字节序(大端序)</p>
<pre><code>struct sockaddr_in sin;
sin.sin_port = htons(40713);
</code></pre>
<p><strong>什么是字节序?</strong></p>
<p>字节序指计算机存储多字节数据(如16位/32位整数)的顺序,分为两种:</p>
<ul>
<li>小端序(Little-Endian):低位字节在前(常见于x86 CPU)。</li>
</ul>
<blockquote>
<ul>
<li>例如:40713(十六进制 0x9F09)在内存中存储为 09 9F(低字节 0x09 在前)。</li>
</ul>
</blockquote>
<ul>
<li>大端序(Big-Endian):高位字节在前(网络标准、PowerPC等)。</li>
</ul>
<blockquote>
<ul>
<li>同一数值存储为 9F 09(高字节 0x9F 在前)</li>
</ul>
</blockquote>
<p>操作系统采用小端序(Little-Endian)主要是由于历史原因和硬件设计优化,其优势体现在数据处理的效率和硬件设计的简化上。(我就不复制粘贴了,反正都是AI告诉我的)</p>
<p><strong>TCP 粘包问题</strong><br>
TCP 是面向字节流的协议,它不保留应用层消息的边界,因此会导致粘包(Packet Sticking)问题。</p>
<p><strong>什么是粘包?</strong><br>
粘包是指发送方多次调用 send() 发送的数据,在接收方的一次 recv() 中全部收到,导致多条消息“粘”在一起,无法区分原始消息边界。</p>
<p>示例<br>
发送方:</p>
<pre><code>send(sockfd, "Hello", 5, 0);// 发送 "Hello"
send(sockfd, "World", 5, 0);// 发送 "World"
</code></pre>
<p>接收方:</p>
<pre><code>char buf;
recv(sockfd, buf, sizeof(buf), 0);// 可能收到 "HelloWorld"(粘包)
</code></pre>
<p>粘包的原因</p>
<ol>
<li> TCP 是字节流协议</li>
</ol>
<ul>
<li>不维护消息边界:TCP 只保证数据按顺序到达,不区分 send() 的调用次数。</li>
<li>数据可能合并或拆分:</li>
<li>Nagle 算法:TCP 默认会合并小数据包(减少网络开销)。</li>
<li>内核缓冲区机制:send() 的数据可能被拆分成多个 TCP 段,或合并成一个段发送。</li>
</ul>
<ol start="2">
<li>接收方缓冲区读取方式</li>
</ol>
<ul>
<li>recv() 读取的是当前接收缓冲区中的所有可用数据,无法自动区分原始消息。</li>
</ul>
<p>粘包的解决方案</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>适用场景</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>固定长度</td>
<td>简单二进制协议</td>
<td>解析快,无需转义</td>
<td>浪费带宽</td>
</tr>
<tr>
<td>分隔符</td>
<td>文本协议(如HTTP)</td>
<td>灵活,人类可读</td>
<td>需处理转义</td>
</tr>
<tr>
<td>长度前缀</td>
<td>高效二进制协议</td>
<td>精准控制,无浪费</td>
<td>需预定义最大长度</td>
</tr>
</tbody>
</table>
<p><strong>TCP 拆包问题</strong></p>
<p>TCP 拆包(Packet Splitting)是指发送方调用一次 send() 发送的数据,可能被 TCP 协议栈拆分成多个数据包传输,导致接收方需要多次 recv() 才能拼凑出完整消息。</p>
<p><strong>啥是拆包</strong><br>
拆包是指一个完整的应用层消息被 TCP 分成多个数据包发送,接收方需要多次读取才能还原原始数据。<br>
示例<br>
发送方:<br>
<code>send(sockfd, "HelloWorld", 10, 0);// 发送 10 字节</code><br>
接收方:</p>
<pre><code>char buf;
recv(sockfd, buf, 5, 0);// 第一次收到 "Hello"
recv(sockfd, buf + 5, 5, 0);// 第二次收到 "World"
</code></pre><br><br>
来源:https://www.cnblogs.com/sleepy2con/p/18894463
頁:
[1]