余食赘行 發表於 2025-5-24 16:27:00

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, &amp;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>&lt;winsock2.h&gt;、&lt;ws2tcpip.h&gt;</td>
<td>&lt;sys/socket.h&gt;、&lt;netinet/in.h&gt;、&lt;arpa/inet.h&gt;、&lt;unistd.h&gt; 等</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", &amp;sin.sin_addr);

int fd = socket(AF_INET, SOCK_STREAM, 0);        // 选择tcp传输
if (fd &lt; 0)
{
        std::cerr &lt;&lt; "socket";
        return 1;
}
if (connect(fd, (struct sockaddr*)&amp;sin, sizeof(sin)))
{
        std::cerr &lt;&lt; "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 &gt; 0)
{
        n_written = send(fd, cp, remaining, 0);
        if (n_written &lt;= 0)
        {
                std::cerr &lt;&lt; "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 &lt; 0)
        {
                std::cerr &lt;&lt; "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]
查看完整版本: 0.libevent学习笔记,从阻塞式socket开始