Linux 网络编程:从零掌握 TCP Socket 编程(附 C++ 实战)

今天翻到一篇不错的技术分享,看完之后自己也琢磨了一下,把思路梳理记录下来。

🎬 个人主页:道尔柯南

❄专栏传送门:《C语言》《C++》《Linux操作系统》

昙花一现,却等待了整个白昼;蝉鸣一夏,却蛰伏了好几个四季。

这篇目录

三、TCP Socket 核心 API 详解 四、实战一:实现最简单的 TCP Echo Server 五、客户端实现:连接服务器并发送数据 六、理解 TCP 的两个重要细节 七、实战二:从单客户端到多客户端 八、四种服务器模型对比九、扩展:TCP 远程命令执行十、TCP Socket 编程中的常见问题 十一、总结 结尾

一、前言

在学习 Linux 网络编程时,大家经常会遇到一个问题:两台计算机上的程序,究竟是如何进行通信的?

例如:

  • 浏览器如何向 Web 服务器发送请求?
  • 聊天软件如何将消息发送给另一台设备?
  • 客户端如何连接服务器,并接收服务器返回的数据?
  • 一个服务器如何同时处理多个客户端的连接?
要理解这些问题,就需要学习 Socket 编程。

在之前的学习中,大家已经接触过 UDP Socket 编程。与 UDP 不同,TCP 提供面向连接的通信方式,能够提供可靠、有序的字节流传输。这样一来,TCP 被广泛应用于文件传输、网页访问、远程服务等场景。

本文将基于 Linux 下的 C++ TCP Socket 编程,带大家从基础 API 着手,逐步实现一个 Echo Server,并进一步探索多进程、多线程和线程池三种并发处理方案。

二、TCP Socket 编程基础

2.1 什么是 Socket?

Socket 可以理解为应用程序进行网络通信时使用的通信端点。

在 Linux 系统中,调用 socket() 创建 Socket 后,系统会返回一个文件描述符。大家可以使用这个描述符,通过相关系统调用进行网络数据的收发。

这与 Linux 中的文件操作有一定相似性:

  • 普通文件通过文件描述符进行读写。
  • Socket 也可以通过文件描述符进行网络通信。
  • 文件操作通常使用 read()、write() 等接口。
  • TCP Socket 同样可以使用 read()、write() 进行数据收发。
这样一来,可以把 Socket 理解为 Linux 文件描述符机制在网络通信中的一种应用。

2.2 TCP 与 UDP 的区别

对比工程TCPUDP通信方式面向连接无连接传输形式字节流数据报可靠性提供可靠、有序的字节流传输不保证可靠送达是否需要建立连接需要不需要常见接口socket、bind、listen、accept、connectsocket、bind、sendto、recvfrom

需要要注意:TCP 保证的是字节流的可靠、有序传输,并不保证一次 write() 对应对方的一次 read()。如果应用程序需要明确的消息边界,就需要进一步设计应用层通信协议。

2.3 TCP 通信的基本流程

TCP 通信通常由客户端和服务器两部分组成。

服务器负责创建监听 Socket、绑定本地地址、监听连接请求,并通过 accept() 获取客户端连接。

客户端负责创建 Socket,并使用 connect() 主动连接服务器。

连接建立后,双方就可以进行双向数据通信。

TCP 客户端
                  |
                  | socket()
                  v
             创建客户端 Socket
                  |
                  | connect()
                  v
        +----------------------+
        |      TCP 服务器      |
        |                      |
        |     socket()         |
        |         |            |
        |       bind()         |
        |         |            |
        |      listen()        |
        |         |            |
        |      accept()        |
        +----------------------+
                  |
                  v
             建立 TCP 连接
                  |
          +-------+-------+
          |               |
       发送数据         接收数据
          |               |
          +-------+-------+
                  |
               关闭连接

理解这个流程后,我们就可以着手学习 TCP Socket 的核心 API。

三、TCP Socket 核心 API 详解

重点介绍 socket()、bind()、listen()、accept() 和 connect() 这几个接口。

3.1 socket():创建 Socket

socket() 用于创建一个通信端点。

函数原型如下:

#include
#include

int socket(int domain, int type, int protocol);

常见参数如下:

  • domain:指定通信协议族,IPv4 网络使用 AF_INET。
  • type:指定 Socket 类型,TCP 使用 SOCK_STREAM。
  • protocol:指定具体协议,通常传入 0,由系统根据前两个参数选择默认协议。
创建 TCP Socket 的代码如下:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0)
{
    perror("socket");
    return 1;
}

如果创建成功,socket() 返回一个非负文件描述符;如果失败,则返回 -1。

这里需要记住:

TCP Socket 的典型创建方式是 socket(AF_INET, SOCK_STREAM, 0)。

3.2 bind():绑定 IP 地址和端口

服务器通常需要监听一个固定的 IP 地址和端口,这样一来需要调用 bind(),将 Socket 与本地网络地址关联起来。

函数原型:

int bind(int sockfd,
         const struct sockaddr *addr,
         socklen_t addrlen);

其中:

  • sockfd:需要绑定的 Socket 文件描述符。
  • addr:需要绑定的网络地址。
  • addrlen:地址结构体的长度。
在 IPv4 网络中,通常使用 sockaddr_in 描述网络地址。

struct sockaddr_in local;

memset(&local, 0, sizeof(local));

local.sin_family = AF_INET;
local.sin_port = htons(9999);
local.sin_addr.s_addr = htonl(INADDR_ANY);

逐行理解:

第一步:设置地址类型。

local.sin_family = AF_INET;

表示使用 IPv4 地址。

第二步:设置端口号。

local.sin_port = htons(9999);

表示服务器使用 9999 端口。

网络通信中存在主机字节序和网络字节序的区别,因此需要使用 htons() 将主机字节序的 16 位端口号转换为网络字节序。

第三步:设置监听地址。

local.sin_addr.s_addr = htonl(INADDR_ANY);

INADDR_ANY 表示绑定本机的任意 IPv4 地址,适用于希望服务器通过本机多个 IPv4 网络接口接收连接的情况。

最后调用 bind():

if (bind(sockfd,
         (struct sockaddr *)&local,
         sizeof(local)) != 0)
{
    perror("bind");
    return 1;
}

这里的类型转换是因为 bind() 接收的是通用地址类型 sockaddr,而我们实际使用的是 IPv4 地址结构体 sockaddr_in。

使用 CONV() 宏封装这一转换:

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

这样可以简化后续的代码。

3.3 listen():让服务器进入监听状态

listen() 用于将绑定后的 TCP Socket 设置为监听状态,使其能够等待客户端发起连接。

函数原型:

int listen(int sockfd, int backlog);

例如:

if (listen(sockfd, 6) != 0)
{
    perror("listen");
    return 1;
}

参数说明:

  • sockfd:服务器的监听 Socket。
  • backlog:用于设置连接等待队列的相关限制。
需要要注意,backlog 不是服务器最多只能服务多少个客户端的简单上限。它主要与等待被接受的连接队列有关,实际行为还受操作系统和连接状态等因素影响。

listen() 成功后,服务器便进入等待客户端连接的阶段。

3.4 accept():获取客户端连接

accept() 用于从监听 Socket 对应的连接队列中取出一个已经完成连接建立的客户端连接。

函数原型:

int accept(int sockfd,
           struct sockaddr *addr,
           socklen_t *addrlen);

示例代码:

struct sockaddr_in peer;
socklen_t len = sizeof(peer);

int sockfd = accept(
    listensock,
    (struct sockaddr *)&peer,
    &len
);

if (sockfd < 0)
{
    perror("accept");
    return 1;
}

这里有一个非常重要的知识点:监听 Socket 和通信 Socket 不是同一个 Socket。

  • listensock:负责等待和接收新的客户端连接。
  • sockfd:由 accept() 返回,用于与某个具体客户端进行通信。
可以将它们理解为饭店中的两个角色:

监听 Socket 就像负责接待顾客的前台;accept() 获取连接后返回的新 Socket,则像分配给某位顾客的专属服务通道。

服务器可以继续使用监听 Socket 接待新的客户端,而已经建立的连接则使用各自的通信 Socket 进行数据收发。

另外,accept() 在没有可接收的连接时,通常会阻塞等待。

3.5 connect():客户端主动连接服务器

与服务器的 bind() 不同,客户端通常通过 connect() 指定服务器的 IP 地址和端口,主动发起连接。

函数原型:

int connect(int sockfd,
            const struct sockaddr *addr,
            socklen_t addrlen);

示例代码:

struct sockaddr_in server;

memset(&server, 0, sizeof(server));

server.sin_family = AF_INET;
server.sin_port = htons(9999);

inet_pton(AF_INET,
          "127.0.0.1",
          &server.sin_addr);

if (connect(sockfd,
            (struct sockaddr *)&server,
            sizeof(server)) < 0)
{
    perror("connect");
    return 1;
}

这里使用了 inet_pton(),将字符串形式的 IPv4 地址转换为网络地址格式。

例如:

"127.0.0.1"

表示本机的回环地址,适合在同一台计算机上测试客户端和服务器。

客户端通常不需要显式调用 bind()。操作系统可以在连接过程中自动为客户端分配合适的本地 IP 地址和临时端口。

3.6 read() 和 write():收发数据

连接建立后,双方可以使用 read() 和 write() 进行通信。

客户端发送数据:

write(sockfd, message, message_length);

服务器接收数据:

char buffer[1024];

ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);

读取后需要判断返回值:

```
if (n > 0)
{
buffer[n] = '\0';
std::cout


这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。

评论 (0)

暂无评论