Socket(套接字)是程序与网络之间的接口。一个程序当服务器、监听端口,另一个当客户端、主动连上来,两边用 socket 收发数据,聊天、网页、流媒体背后全是这套。下面讲 C 语言里 socket 怎么建、怎么连、怎么传数据。

什么是 Socket

Socket 是 IP 地址 + 端口号组合出来的一个网络端点,比如 192.168.1.1:8080。客户端和服务器各持一个 socket,通过它们交换数据。

按传输协议分两类:

  • TCP Socket(流式):面向连接、可靠,数据按序到达,像打电话。
  • UDP Socket(数据报):无连接、快但可能丢包,像发快递。

下面先用一个最小例子感受全流程,服务器收一句"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():创建 socket,拿到一个文件描述符。
  2. setsockopt()(可选):设置选项,比如 SO_REUSEADDR 避免"address already in use"。
  3. bind():把 socket 绑到指定 IP 和端口。
  4. listen():进入被动监听,等待客户端连接;backlog 是等待队列长度。
  5. accept():从队列里取出一个连接请求,返回一个新的 socket 描述符用于通信。
  6. send() / recv():用新 socket 收发数据。
  7. close():通信完毕关闭 socket,释放资源。

核心调用签名:

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)、AF_INET6(IPv6);type 用 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP);protocol 一般传 0 表示让系统选默认。

客户端四步走

  1. socket():和服务器一样先建 socket。
  2. connect():用服务器地址和端口发起连接,指定 sockaddr 里存服务器的 IP 和端口。
  3. send() / recv():和服务器收发数据。
  4. close():通信完毕关闭。

UDP 与 TCP 的差异

对比项TCPUDP
连接面向连接,先 connect无连接,直接发数据
可靠性可靠、按序、无重复可能丢包、乱序
速度较慢(有握手、确认)较快
典型场景网页、文件传输、聊天音视频、游戏、实时查询

UDP 不需要 bind 监听,直接 sendto() / recvfrom() 带上目标地址就能收发。

常见问题与排查

  • 连接失败:确认客户端连的 IP 和端口与服务器绑定的一致。
  • 端口占用:bind 报错"address already in use",换端口或等旧进程释放,也可用 SO_REUSEADDR。
  • 阻塞卡住:socket 默认是阻塞的,accept()/recv() 没有连接或数据时会一直等。需要时可把 socket 设成非阻塞。

socket 编程的核心就七个系统调用:socket、bind、listen、accept、send、recv、close。服务器把这七步走一遍,客户端走简化版,网络通信就跑起来了。