套接字(socket)是应用层与传输层之间的编程接口,通过 IP 地址与端口号标识通信端点。服务器在端口上监听,客户端发起连接后双方即可收发字节流。下列示例基于 POSIX 套接字 API(Linux 等);与错误处理、多线程结合可构建并发网络服务。

套接字的概念

套接字可视为网络通信的句柄。按协议划分常见两类:

  • TCP(流式套接字):面向连接、可靠、按序传输字节流。
  • UDP(数据报套接字):无连接,不保证送达与顺序,开销较低。

以下以 TCP 为例:客户端发送 Hello from client,服务器回复 Hello from server。

服务器端 客户端 socket() bind() 绑定端口 listen() 监听 accept() 等待连接 recv() / send() close() socket() connect() 主动连上 send() / recv() close() TCP 三次握手
服务器先 socket→bind→listen→accept 等着,客户端 socket→connect 连上来,之后两边互发数据,最后都 close。

完整示例:TCP 服务器与客户端

server.c:建服务器

#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080

int main(int argc, char const* argv[]) {
    int server_fd, new_socket;
    ssize_t valread;
    struct sockaddr_in address;
    int opt = 1;
    socklen_t addrlen = sizeof(address);
    char buffer[1024] = { 0 };
    char* hello = "Hello from server";

    /* 1. 创建 socket 文件描述符 */
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    /* 2. 允许地址端口复用,避免重启时报 address already in use */
    if (setsockopt(server_fd, SOL_SOCKET,
                   SO_REUSEADDR | SO_REUSEPORT, &opt,
                   sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    /* 3. 绑定地址和端口 */
    if (bind(server_fd, (struct sockaddr*)&address,
             sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    /* 4. 进入监听状态,等待连接 */
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    /* 5. 接受一个客户端的连接 */
    if ((new_socket = accept(server_fd, (struct sockaddr*)&address,
                             &addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    /* 6. 读客户端发来的数据,再回一句 */
    valread = read(new_socket, buffer, 1024 - 1);
    printf("%s\n", buffer);
    send(new_socket, hello, strlen(hello), 0);
    printf("Hello message sent\n");

    /* 7. 关闭连接和监听 socket */
    close(new_socket);
    close(server_fd);
    return 0;
}

client.c:连服务器

#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080

int main(int argc, char const* argv[]) {
    int status, valread, client_fd;
    struct sockaddr_in serv_addr;
    char* hello = "Hello from client";
    char buffer[1024] = { 0 };

    /* 1. 创建 socket */
    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    /* 2. 把字符串形式的 IP 转成二进制 */
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }

    /* 3. 主动连接服务器 */
    if ((status = connect(client_fd, (struct sockaddr*)&serv_addr,
                          sizeof(serv_addr))) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }

    /* 4. 发数据、收回复 */
    send(client_fd, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    valread = read(client_fd, buffer, 1024 - 1);
    printf("%s\n", buffer);

    /* 5. 关闭连接 */
    close(client_fd);
    return 0;
}

编译并运行:

gcc server.c -o server
gcc client.c -o client

./server   &   # 终端 1 先跑服务器
./client      # 终端 2 跑客户端

输出:

客户端:Hello message sent
        Hello from server
服务器:Hello from client
        Hello message sent

服务器端步骤

  1. socket():创建套接字,获得文件描述符。
  2. setsockopt()(可选):如 SO_REUSEADDR 便于快速重启服务。
  3. bind():绑定本地 IP 与端口。
  4. listen():标记为被动监听;backlog 为等待队列长度。
  5. accept():接受连接,返回用于通信的新描述符。
  6. send() / recv()(或 read/write):在新描述符上收发数据。
  7. close():关闭描述符,释放资源。

相关原型:

sockfd = socket(domain, type, protocol);
setsockopt(sockfd, level, optname, optval, optlen);
bind(sockfd, (struct sockaddr*)&addr, addrlen);
listen(sockfd, backlog);
new_socket = accept(sockfd, (struct sockaddr*)&addr, &addrlen);
send(sockfd, buf, len, flags);
recv(sockfd, buf, len, flags);
close(fd);

domain 常用 AF_INET(IPv4);type 为 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP);protocol 常传 0 由系统选择默认协议。

客户端步骤

  1. socket():创建套接字。
  2. connect():向服务器地址发起连接。
  3. send() / recv():交换数据。
  4. close():关闭连接。

UDP 与 TCP 对比

对比项TCPUDP
连接面向连接无连接
可靠性可靠、有序可能丢包、乱序
开销握手与确认带来额外延迟通常更低
典型场景HTTP、文件传输实时音视频、DNS 查询

UDP 常用 sendto / recvfrom 并在每次调用中指定对端地址,无需 listen / accept。

常见问题

  • 连接失败:确认客户端目标 IP、端口与服务器 bind 一致,防火墙未阻断。
  • address already in use:端口仍被占用,可等待释放或设置 SO_REUSEADDR。
  • 阻塞:默认套接字为阻塞模式,accept、recv 无数据时会等待;可按需设为非阻塞或配合 select / poll。

本节介绍了 TCP 套接字的基本流程。继续深入时可结合C11 泛型选择等语言特性组织协议解析与工具库;多连接场景宜结合线程或事件驱动模型设计。