C++网络编程 3.TCP套接字(socket)编程示例程序

TCP套接字示例程序详解:服务器与客户端通信实战

一、程序整体说明

这是一个基于Linux的简易TCP通信示例,包含服务器程序客户端程序 ,实现了"客户端发送消息→服务器接收并回复"的完整流程。通过这个示例可以直观理解TCP套接字编程的核心步骤和API使用方法。

先附上整体程序,再逐步讲解。

server.cpp
cpp 复制代码
/*
简易TCP服务器
*/

#include <iostream>
#include <sys/socket.h> //头文件
#include <netinet/in.h> //操作IP地址的
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main(int, char **)
{
    // 1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);//tcp

    if (listen_sock == -1)
    {
        std::cout << "failed to create socket" << std::endl;
        return 1;
    }

    // 2.绑定IP地址
    // IP:设备,端口号:进程
    std::string ip = "127.0.0.1";
    struct sockaddr_in server_addr;               // 数据类型sockaddr_in
    memset(&server_addr, 0, sizeof(server_addr)); // 地址滞空(清空)
    // 指定地址类型
    server_addr.sin_family = AF_INET; // 指定IPV4
    // 指定IP地址
    server_addr.sin_addr.s_addr = INADDR_ANY; // 可以监听所有IP地址
    // 指定端口号
    server_addr.sin_port = htons(9888);//转类型,网络通信 (h to n 本机到网络)

    // server_addr需要将类型转换为struct sockaddr*,因为sockaddr_in更好赋值所以用in
    if (bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        std::cout << "failed to bind socket" << std::endl;
        return 1;
    }

    // 3.监听套接字
    if (listen(listen_sock, 5) == -1)
    {
        std::cerr << "failed to listen" << std::endl;
        return 1;
    }
    std::cout << "server is listening" << std::endl;

    // 4.接受客户端连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    // 客户端socket
    //创建一个新的socket文件描述符来处理客户端的请求
    int client_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_addr_len);
    if (client_sock == -1)
    {
        std::cerr << "failed to accept" << std::endl;
        return 1;
    }

    // 5.数据交互

    // 5.1接受消息
    char buffer[1024];
    // 读取客户端消息,存入buffer里
    int resd_size = read(client_sock, buffer, sizeof(buffer)); // 对应客户端recv

    std::cout << "Recerved msg: \n" << buffer << std::endl;

    // 5.2发送消息
    std::string res_msg = "hello client !";
    write(client_sock, res_msg.c_str(), res_msg.size()); // 对应客户端send

    // 6.关闭socket
    close(client_sock);
    close(listen_sock);
}
client.cpp
cpp 复制代码
#include <iostream>
#include <sys/socket.h> //头文件
#include <netinet/in.h> //操作IP地址的
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
using namespace std;
int main()
{
    // 1.创建socket
    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock == -1)
    {
        std::cerr << "failed to create socket" << std::endl;
        return -1;
    }

    // 2.连接服务器
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    // 在网络编程中,需要把字符串变成网络字节序
    //(1)老写法
    //  server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//本机
    //(2)通用写法
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr.s_addr);
    server_addr.sin_port = htons(9888);

    // 用connect()函数连接
    if (connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        std::cerr << "failed to connect" << std::endl;
        return -1;
    }

    cout << "connected to server" << endl;

    // 3.数据交互
    string msg = "hello world !\n";

    if(write(client_sock, msg.c_str(), msg.length())==-1)
    {
        std::cerr << "failed to write" << std::endl;
        return -1;
    }

    //接收消息
    char buffer[1024];
    if(read(client_sock , buffer,sizeof(buffer)) == -1)
    {
        std::cerr << "failed to read" << std::endl;
        return -1;
    }

    printf("received to server:\n %s" , buffer);
    close(client_sock);

    return 0;
}

二、服务器程序详解(server.cpp)

服务器的核心任务是"绑定端口→监听连接→接受客户端→收发数据→关闭连接",代码流程与TCP通信理论完全对应。

1. 头文件与初始化

cpp 复制代码
#include <iostream>
#include <sys/socket.h>  // 套接字核心API(socket/bind/listen等)
#include <netinet/in.h>  // 定义sockaddr_in等地址结构体
#include <arpa/inet.h>   // 提供IP地址转换函数(inet_pton/ntohs等)
#include <unistd.h>      // 提供close/read/write等系统调用
#include <cstring>       // 提供memset等字符串操作函数

这些头文件是Linux网络编程的基础,包含了所有必需的函数和结构体定义。

2. 步骤1:创建套接字(socket())

cpp 复制代码
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);  // 创建TCP套接字
if (listen_sock == -1) {
    std::cout << "failed to create socket" << std::endl;
    return 1;
}
  • 作用:创建一个用于网络通信的套接字描述符(类似文件描述符),是后续所有操作的基础。
  • 参数解析
    • AF_INET:使用IPv4协议族;
    • SOCK_STREAM:创建流式套接字(对应TCP协议,可靠连接);
    • 0:使用默认协议(TCP)。
  • 错误处理 :若返回 -1 表示创建失败(如权限不足、系统资源耗尽),需退出程序。

3. 步骤2:绑定IP和端口(bind())

cpp 复制代码
struct sockaddr_in server_addr;  // IPv4地址结构体
memset(&server_addr, 0, sizeof(server_addr));  // 初始化结构体(清零)
server_addr.sin_family = AF_INET;  // 协议族(必须与socket()一致)
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定所有本地IP
server_addr.sin_port = htons(9888);  // 绑定端口(转换为网络字节序)

// 绑定套接字与地址
if (bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    std::cerr << "failed to bind socket" << std::endl;
    return 1;
}
  • 核心结构体 sockaddr_in :专门用于IPv4地址存储,需强制转换为通用的 struct sockaddr* 传给 bind()
  • 关键参数解析
    • sin_family:必须设置为 AF_INET(与socket协议族一致);
    • sin_addr.s_addrINADDR_ANY 表示绑定本地所有网络接口的IP(如服务器有多个网卡时,可接收任意网卡的连接),htonl() 用于将整数转换为网络字节序;
    • sin_port:端口号(9888),需通过 htons() 转换为网络字节序(TCP协议规定端口和IP必须用网络字节序传输)。
  • 作用:将创建的套接字与"本地IP+端口"绑定,使客户端能通过该端口找到服务器。

4. 步骤3:监听连接(listen())

cpp 复制代码
if (listen(listen_sock, 5) == -1) {  // 第二个参数backlog=5
    std::cerr << "failed to listen" << std::endl;
    return 1;
}
std::cout << "server is listening" << std::endl;
  • 作用:将套接字设置为"监听状态",允许接收客户端的连接请求。
  • 参数 backlog:表示未完成连接队列的最大长度(处于三次握手未完成状态的连接数),超过则新连接被拒绝(这里设为5)。
  • 注意listen() 不阻塞,仅标记套接字为监听状态,真正等待连接的是 accept()

5. 步骤4:接受客户端连接(accept())

cpp 复制代码
struct sockaddr_in client_addr;  // 存储客户端地址信息
socklen_t client_addr_len = sizeof(client_addr);  // 地址长度(输入输出参数)

// 阻塞等待客户端连接,返回与该客户端通信的套接字
int client_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1) {
    std::cerr << "failed to accept" << std::endl;
    return 1;
}
  • 核心特性accept()阻塞函数,若没有客户端连接,会一直等待(直到有连接或被信号中断)。
  • 参数与返回值
    • 输入:监听套接字 listen_sock、客户端地址结构体指针 client_addr、地址长度指针 client_addr_len
    • 返回:新的通信套接字 client_sock ,专门用于与当前客户端收发数据(原监听套接字 listen_sock 仍可继续接受其他连接)。
  • 作用:完成TCP三次握手,建立与客户端的连接,返回通信套接字。

6. 步骤5:数据交互(read()/write())

6.1 接收客户端消息(read())
cpp 复制代码
char buffer[1024];  // 数据缓冲区(存储接收的消息)
// 从客户端套接字读取数据,存入buffer
int read_size = read(client_sock, buffer, sizeof(buffer));  
std::cout << "Received msg: \n" << buffer << std::endl;
  • 作用 :从客户端的通信套接字读取数据(客户端发送的消息存于内核读缓冲区,read() 将其拷贝到用户态的 buffer)。
  • 参数 :通信套接字 client_sock、缓冲区 buffer、缓冲区大小 sizeof(buffer)
  • 注意read() 可能返回实际读取的字节数(小于缓冲区大小),此处简化处理未判断返回值(实际开发需检查)。
6.2 向客户端发送消息(write())
cpp 复制代码
std::string res_msg = "hello client !";  // 要回复的消息
// 向客户端套接字写入数据(发送到内核写缓冲区,内核异步发送到网络)
write(client_sock, res_msg.c_str(), res_msg.size());  
  • 作用:将服务器的回复消息写入通信套接字的写缓冲区,由内核发送到客户端。
  • 参数 :通信套接字 client_sock、消息指针 res_msg.c_str()、消息长度 res_msg.size()

7. 步骤6:关闭套接字(close())

cpp 复制代码
close(client_sock);   // 关闭与客户端的通信套接字(触发四次挥手)
close(listen_sock);   // 关闭监听套接字(停止接受新连接)
  • 作用:释放套接字资源,关闭TCP连接(内核会完成四次挥手流程)。

三、客户端程序详解(client.cpp)

客户端的核心任务是"创建套接字→连接服务器→收发数据→关闭连接",流程比服务器更简洁(无需绑定和监听)。

1. 步骤1:创建套接字(socket())

cpp 复制代码
int client_sock = socket(AF_INET, SOCK_STREAM, 0);  // 创建TCP套接字
if (client_sock == -1) {
    std::cerr << "failed to create socket" << std::endl;
    return -1;
}
  • 与服务器 socket() 用法完全一致:创建流式套接字,返回客户端套接字描述符。

2. 步骤2:连接服务器(connect())

cpp 复制代码
struct sockaddr_in server_addr;  // 服务器地址结构体
server_addr.sin_family = AF_INET;  // 协议族(IPv4)

// 设置服务器IP地址(字符串→网络字节序二进制)
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr.s_addr);  
// 设置服务器端口(主机字节序→网络字节序)
server_addr.sin_port = htons(9888);  

// 向服务器发起连接请求(完成三次握手)
if (connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    std::cerr << "failed to connect" << std::endl;
    return -1;
}
cout << "connected to server" << endl;
  • 核心函数 connect():客户端发起TCP三次握手,与服务器建立连接。
  • 地址设置关键
    • IP地址转换:inet_pton(AF_INET, "127.0.0.1", ...) 将点分十进制字符串(127.0.0.1 是本地回环地址)转换为网络字节序的二进制IP(现代推荐用法,替代老的 inet_addr());
    • 端口转换:htons(9888) 将端口号转换为网络字节序(必须与服务器绑定的端口一致)。
  • 阻塞特性connect() 会阻塞直到连接建立或失败(如服务器未启动则返回 -1)。

3. 步骤3:数据交互(write()/read())

3.1 向服务器发送消息(write())
cpp 复制代码
string msg = "hello world !\n";  // 要发送的消息
// 向服务器套接字写入数据(发送到内核写缓冲区)
if(write(client_sock, msg.c_str(), msg.length()) == -1) {  
    std::cerr << "failed to write" << std::endl;
    return -1;
}
  • 作用与服务器的 write() 一致:将消息发送到服务器。
3.2 接收服务器回复(read())
cpp 复制代码
char buffer[1024];  // 存储服务器回复的缓冲区
// 从服务器套接字读取回复消息
if(read(client_sock, buffer, sizeof(buffer)) == -1) {  
    std::cerr << "failed to read" << std::endl;
    return -1;
}
printf("received from server:\n %s" , buffer);  // 打印服务器回复
  • 作用与服务器的 read() 一致:读取服务器发送的回复消息。

4. 步骤4:关闭套接字(close())

cpp 复制代码
close(client_sock);  // 关闭客户端套接字,触发四次挥手

四、核心知识点总结

1. 套接字角色区分

  • 服务器 有两个套接字:
    • 监听套接字(listen_sock):仅用于绑定、监听、接受连接,不参与数据交互;
    • 通信套接字(client_sock):每个客户端连接对应一个,专门用于收发数据。
  • 客户端 只有一个套接字(client_sock):用于连接服务器和数据交互。

2. 网络字节序转换的必要性

  • 不同计算机的主机字节序(大端/小端)可能不同,TCP协议规定网络中必须使用大端字节序 ,因此:
    • 端口号必须用 htons() 转换(host to network short,16位);
    • IP地址(32位)必须用 htonl() 转换(host to network long)。

3. 数据交互的缓冲区机制

  • 数据通过内核缓冲区间接传输:
    • 发送:write()/send() 将数据写入写缓冲区,内核异步发送到网络;
    • 接收:内核将网络数据存入读缓冲区read()/recv() 从缓冲区读取数据。

4. 程序局限性(优化方向)

  • 仅支持单个客户端accept() 只调用一次,处理完一个客户端后程序就退出(实际服务器需循环 accept() 并通过多线程/多进程处理多个客户端);
  • 未处理粘包问题:若客户端发送大数据或连续发送小数据,服务器可能无法正确解析边界(需用"长度前缀法"优化);
  • 错误处理简化:未严格检查 read()/write() 的返回值(可能返回0表示连接关闭,-1表示错误);
  • 无循环通信:仅一次收发就结束,实际应用需循环接收消息(如 while 循环)。

五、运行流程演示

  1. 编译程序

    bash 复制代码
    g++ server.cpp -o server  # 编译服务器
    g++ client.cpp -o client  # 编译客户端
  2. 启动服务器

    bash 复制代码
    ./server  # 输出 "server is listening",阻塞等待连接
  3. 启动客户端 (新终端):

    bash 复制代码
    ./client  # 输出 "connected to server",发送消息并接收回复
  4. 预期输出

    • 服务器终端:Received msg: hello world !
    • 客户端终端:received from server: hello client !

通过这个示例,能直观理解TCP通信的全流程,后续可基于此扩展为支持多客户端、循环通信的完整服务器。