4. TCP Socket编程(一):服务端与客户端创建流程

各位同学,今天我们来聊聊TCP Socket编程。说实话,这是网络编程里最基础也最核心的一块。我记得刚入行那会儿,第一次写Socket程序,折腾了一整天都没跑通——后来发现是bind的端口被占用了。嗯,这种坑踩过一次就记住了。

4.1 服务端创建流程:socket → bind → listen → accept

服务端的创建,说白了就是四个步骤。我习惯把这四个步骤记成「开、绑、听、接」——开一个socket,绑一个地址,听一个端口,接一个连接。咱们一步步来看。

4.1.1 socket():创建套接字

第一步,创建socket。这个函数就像是在操作系统里申请一个「网络通信的管道口」。

#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET:IPv4协议族。我个人习惯用AF_INET6表示IPv6,但大部分场景还是IPv4。
  • SOCK_STREAM:流式套接字,对应TCP。如果是UDP就用SOCK_DGRAM。
  • 0:协议自动选择,TCP的话填0就行。
注意:socket()返回的是文件描述符。如果返回-1,说明创建失败。我在项目中遇到过一种情况——进程的文件描述符上限被设得太低,导致socket创建失败。排查了半天才发现是ulimit的问题。

4.1.2 bind():绑定地址和端口

第二步,绑定。你得告诉操作系统:「我这个socket要监听哪个IP和哪个端口」。

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网卡
server_addr.sin_port = htons(8888);               // 端口8888

bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

这里有几个细节要注意:

  • htonl()和htons():主机字节序转网络字节序。说白了就是大小端转换。不同CPU存数据的方式不一样,网络传输统一用大端序。
  • INADDR_ANY:表示监听本机所有IP地址。如果你只想监听某个特定IP,可以填具体的地址。
  • 端口号:小于1024的端口需要root权限。我建议开发阶段用8000-9000之间的端口,不容易冲突。
小技巧:bind之前可以设置SO_REUSEADDR选项,这样即使端口被占用也能重新绑定。我曾经在调试时频繁重启服务,没加这个选项就得等2分钟才能重新bind,特别烦人。

4.1.3 listen():进入监听状态

第三步,监听。bind之后,socket还不会主动接收连接。你得调用listen,告诉内核:「我准备好了,有连接就通知我」。

listen(sockfd, 5);  // 第二个参数是backlog,表示连接队列的最大长度

backlog这个参数,很多人不理解。它其实是指「已完成三次握手但还没被accept取走的连接」的最大数量。如果队列满了,新的连接请求会被拒绝。

我建议backlog不要设得太小,也不要太大。太小了高并发时容易丢连接,太大了浪费内存。一般设128或256就够用了。

4.1.4 accept():接受客户端连接

第四步,接受连接。accept是个阻塞函数——如果没有客户端连上来,它会一直等在那里。

struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);

accept返回的是一个新的文件描述符,专门用来和这个客户端通信。原来的sockfd还在继续监听,可以接受其他连接。

核心理解:服务端有两个socket——一个是「监听socket」,负责接客;另一个是「通信socket」,负责和具体某个客户端聊天。这两个角色要分清楚。

4.2 客户端创建流程:socket → connect

客户端的流程就简单多了,两步走:创建socket,然后连接服务端。

4.2.1 socket():创建套接字

和服务端一样,客户端也要先创建一个socket。

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

这里没什么特别的,参数和服务端完全一致。

4.2.2 connect():发起连接

connect函数负责发起三次握手。它会主动连接服务端的IP和端口。

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务端IP
server_addr.sin_port = htons(8888);                     // 服务端端口

connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

connect也是阻塞的。它会一直等到三次握手完成,或者超时失败。如果服务端没启动,connect会返回-1,errno设置为ECONNREFUSED。

避坑指南:我曾经遇到过一个问题——客户端connect总是超时,但服务端明明在运行。后来发现是防火墙把端口给拦了。所以调试时先试试telnet,能通说明网络没问题。

4.3 简单回显服务器实现

好了,理论讲完了,咱们写个完整的例子。回显服务器(echo server)是最简单的TCP应用——客户端发什么,服务端就原样返回什么。

4.3.1 服务端代码

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

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 1. 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket failed");
        exit(1);
    }

    // 2. 设置地址重用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(1);
    }

    // 4. 监听
    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(1);
    }

    printf("Echo server listening on port %d...\n", PORT);

    // 5. 接受连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("accept failed");
        close(server_fd);
        exit(1);
    }

    printf("Client connected!\n");

    // 6. 回显逻辑
    int n;
    while ((n = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
        write(client_fd, buffer, n);
    }

    // 7. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

4.3.2 客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket failed");
        exit(1);
    }

    // 2. 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(PORT);

    // 3. 连接服务端
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        close(sockfd);
        exit(1);
    }

    printf("Connected to server!\n");

    // 4. 发送数据并接收回显
    while (1) {
        printf("Enter message: ");
        fgets(buffer, BUFFER_SIZE, stdin);

        write(sockfd, buffer, strlen(buffer));

        int n = read(sockfd, buffer, BUFFER_SIZE);
        if (n <= 0) break;

        buffer[n] = '\0';
        printf("Echo: %s", buffer);
    }

    close(sockfd);
    return 0;
}

4.4 核心流程图

下面这张图展示了TCP Socket编程的完整流程。我特意把服务端和客户端放在一起对比,方便你理解。

TCP Socket编程核心流程 服务端 (Server) 1. socket() 创建套接字 2. bind() 绑定地址和端口 3. listen() 进入监听状态 4. accept() 阻塞等待连接 5. read() / write() 通信 6. close() 关闭连接 客户端 (Client) 1. socket() 创建套接字 2. connect() 发起连接 3. write() / read() 通信 4. close() 关闭连接 三次握手

4.5 关键点总结

函数 作用 注意事项
socket() 创建套接字 返回文件描述符,失败返回-1
bind() 绑定地址和端口 端口小于1024需root权限
listen() 进入监听状态 backlog控制连接队列长度
accept() 接受客户端连接 阻塞函数,返回新fd
connect() 发起连接 阻塞直到三次握手完成
我的建议:初学者先把这个回显服务器跑通,然后试着改一改——比如改成并发处理多个客户端,或者加个心跳检测。我当年就是靠不断改这个小demo,才真正理解了TCP Socket的运作机制。

好了,这一章的内容就到这儿。代码不多,但每一步都很关键。你想想看,整个互联网的通信,底层不就是靠这些socket在支撑吗?把这个基础打牢了,后面讲多路复用、线程池、异步IO的时候,你才能跟得上。

可可精品资料库