TCP网络编程:从入门到精通

文章目录

  • [Echo server](#Echo server)
    • [1.1 功能介绍](#1.1 功能介绍)
    • [1.2 TCP核心接口](#1.2 TCP核心接口)
      • [1. socket():创建套接字(通信端点)](#1. socket():创建套接字(通信端点))
      • [2. bind():绑定地址和端口](#2. bind():绑定地址和端口)
      • [3. listen():监听连接(仅服务器端)](#3. listen():监听连接(仅服务器端))
      • [4. accept():接受连接(仅服务器端)](#4. accept():接受连接(仅服务器端))
      • [5. connect():发起连接(仅客户端)](#5. connect():发起连接(仅客户端))
      • [6. read()/write() / recv()/send():收发数据](#6. read()/write() / recv()/send():收发数据)
      • [7. close():关闭连接](#7. close():关闭连接)
      • [8. 总结](#8. 总结)
    • [1.3 demo1 v1](#1.3 demo1 v1)
    • [1.4 实验结果](#1.4 实验结果)
    • [1.5 流程详解](#1.5 流程详解)
      • [1.5.1 关于listen() 函数的核心作用](#1.5.1 关于listen() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键注意事项](#4. 关键注意事项)
        • [5. listen() 调用后的套接字状态变化](#5. listen() 调用后的套接字状态变化)
        • [6. 总结](#6. 总结)
      • [1.5.2 accept() 函数的核心作用](#1.5.2 accept() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键特性:阻塞/非阻塞行为](#4. 关键特性:阻塞/非阻塞行为)
        • [5. 常见错误与注意事项](#5. 常见错误与注意事项)
          • [1. 高频错误原因](#1. 高频错误原因)
          • [2. 重要注意事项](#2. 重要注意事项)
        • [6. 总结](#6. 总结)
      • [1.5.3 connect() 函数的核心作用](#1.5.3 connect() 函数的核心作用)
        • [1. 标准原型](#1. 标准原型)
        • [2. 参数详解](#2. 参数详解)
        • [3. 返回值](#3. 返回值)
        • [4. 关键特性:阻塞/非阻塞行为](#4. 关键特性:阻塞/非阻塞行为)
        • [5. 常见错误与注意事项](#5. 常见错误与注意事项)
          • [1. 高频错误原因](#1. 高频错误原因)
          • [2. 重要注意事项](#2. 重要注意事项)
        • [6. 总结](#6. 总结)
    • [demo1 v2](#demo1 v2)
    • [demo1 v3](#demo1 v3)
  • demo2

本文主要是关于UDP网络通信编程的两个demo,通过这两个demo完全掌握UDP网络通信。

Echo server

1.1 功能介绍

功能:简单的回显服务器和客戶端代码,客户端向服务器发送消息,服务器接受消息之后向客户端回显。

1.2 TCP核心接口

TCP 是面向连接的可靠传输协议,基于它的网络编程(以 Linux/Unix 环境为例)遵循经典的 C/S(客户端/服务器)模型,核心接口围绕"创建套接字-绑定地址-监听-接受连接-收发数据-关闭连接"这一流程展开。下面按使用顺序逐一介绍核心接口:

1. socket():创建套接字(通信端点)

这是所有网络编程的第一步,用于创建一个"套接字文件描述符",相当于为网络通信打开一个"通道"。

  • 函数原型

    c 复制代码
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
  • 参数说明

    • domain:地址族,TCP 用 AF_INET(IPv4)或 AF_INET6(IPv6);
    • type:套接字类型,TCP 必须用 SOCK_STREAM(流式套接字,对应面向连接的可靠传输);
    • protocol:协议类型,TCP 填 0(系统会自动匹配 SOCK_STREAM 对应的 TCP 协议)。
  • 返回值 :成功返回非负的文件描述符,失败返回 -1

  • 示例

    c 复制代码
    // 创建TCP套接字(IPv4)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket failed"); // 错误提示
        exit(1);
    }

2. bind():绑定地址和端口

将创建的套接字与服务器的 IP 地址、端口号绑定,让客户端能找到对应的服务器。

  • 函数原型

    c 复制代码
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfdsocket() 返回的套接字描述符;
    • addr:指向包含 IP 和端口的地址结构体(IPv4 用 struct sockaddr_in);
    • addrlen:地址结构体的长度。
  • 示例

    c 复制代码
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本机IP
    server_addr.sin_port = htons(8080); // 绑定8080端口(htons转换字节序)
    
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);
        exit(1);
    }

3. listen():监听连接(仅服务器端)

将绑定后的套接字转为"监听套接字",开始等待客户端的连接请求。

  • 函数原型

    c 复制代码
    int listen(int sockfd, int backlog);
  • 参数说明

    • sockfd:绑定后的套接字描述符;
    • backlog:未处理的连接请求队列的最大长度(如 5,现代系统会自动调整)。
  • 示例

    c 复制代码
    if (listen(sockfd, 5) == -1) {
        perror("listen failed");
        close(sockfd);
        exit(1);
    }
    printf("Server listening on port 8080...\n");

4. accept():接受连接(仅服务器端)

从监听队列中取出一个已完成的客户端连接,返回一个新的套接字描述符(专门用于和该客户端通信),原监听套接字仍继续监听其他连接。

  • 函数原型

    c 复制代码
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数说明

    • sockfd:监听套接字描述符;
    • addr:输出参数,存储客户端的 IP 和端口信息(可填 NULL 表示不关心);
    • addrlen:输入输出参数,传入 addr 结构体的长度,返回实际使用的长度。
  • 示例

    c 复制代码
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    // 阻塞等待客户端连接
    int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
    if (connfd == -1) {
        perror("accept failed");
        close(sockfd);
        exit(1);
    }
    // 打印客户端信息
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
    printf("Client connected: %s:%d\n", client_ip, ntohs(client_addr.sin_port));

5. connect():发起连接(仅客户端)

客户端通过该接口向服务器发起 TCP 连接请求,完成三次握手。

  • 函数原型

    c 复制代码
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfd:客户端创建的套接字描述符;
    • addr:服务器的 IP 和端口地址结构体;
    • addrlen:地址结构体长度。
  • 示例

    c 复制代码
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    // 将服务器IP(如127.0.0.1)转为网络字节序
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
    
    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(client_fd);
        exit(1);
    }
    printf("Connected to server successfully!\n");

6. read()/write() / recv()/send():收发数据

连接建立后,服务器和客户端通过 accept()/connect() 返回的套接字描述符收发数据:

  • read()/write():通用的文件读写接口,可直接用于套接字(因为套接字是文件描述符);

  • recv()/send():专门的网络数据收发接口,支持额外参数(如 MSG_NOSIGNAL 避免断连触发信号)。

  • 示例(服务器接收+发送)

    c 复制代码
    char buf[1024];
    // 接收客户端数据
    ssize_t n = read(connfd, buf, sizeof(buf)-1);
    if (n <= 0) {
        perror("read failed");
        close(connfd);
        return;
    }
    buf[n] = '\0'; // 加字符串结束符
    printf("Received from client: %s\n", buf);
    
    // 向客户端发送响应
    const char *resp = "Hello from server!";
    write(connfd, resp, strlen(resp));

7. close():关闭连接

释放套接字描述符,终止 TCP 连接(会触发四次挥手)。

  • 函数原型

    c 复制代码
    int close(int fd);
  • 示例

    c 复制代码
    close(connfd); // 关闭和客户端的通信套接字
    close(sockfd); // 关闭服务器的监听套接字

8. 总结

  1. TCP 编程核心流程:服务器 socket() → bind() → listen() → accept() → 收发数据 → close();客户端 socket() → connect() → 收发数据 → close()
  2. 关键区分:listen() 仅标记套接字为"监听状态",accept() 才真正接收连接并返回新的通信套接字(原监听套接字可复用)。
  3. 核心接口的作用:socket 创建通道,bind 绑定地址,listen 开启监听,accept/connect 建立连接,read/write 传输数据,close 释放资源。

1.3 demo1 v1

v1版本仅能实现单进程连接访问服务器,仅仅是作为框架演示。
server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            Service(sockfd, addr);
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

server.cc

cpp 复制代码
#include "TcpServer.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << "port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Run();

    return 0;
}

client.hpp

cpp 复制代码
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"

        
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}

// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(SOCKET_ERR);
    }

    // 2. 发起连接
    InetAddr serveraddr(serverip, serverport);
    int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    // 3. echo client
    while(true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char buffer[1024];
        ssize_t size = read(sockfd, buffer, sizeof(buffer)-1);
        if(size > 0)
        {
            buffer[size] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

InetAddr.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr() {}
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
    }
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

1.4 实验结果

1.5 流程详解

先看服务端代码

先判断运行服务器方式是否正确。

使用智能指针创建服务器对象,传参数就是为服务器指定的端口。

执行初始化方法。

先搂一眼服务器的成员变量,包括端口号,监听文件描述符,运行状态。

ok紧着执行初始化方法

首先创建套接字,这里的第二个参数变成了SOCK_STREAM表示流式套接字,是TCP专用。

判断是否创建成功,

输出日志信息。

这里还封装了common类,用来表示退出信息。

手动绑定端口号,绑定之前要先填写socket信息,这里专门封装了InetAddr类完成。

关于InetAddr类:

三个参数就是socket信息

重载的四个构造函数,这里用的最后一个:

清空结构体,填写家族协议,IP,端口号。

对于IP不需要手动绑定,服务器可能有多个网卡(有线/无线/内网/外网IP),绑特定IP只会响应该网卡的请求,其他网卡的请求会被丢弃;绑INADDR_ANY能接收所有网卡的请求。

对于端口号,需要将本地主机序列转化为网络序列。


这里对强转做了包装,方便bind函数的第二三参数的填写。

判断是否绑定成功,用日志输出。

以上和UDP其实都差不多...

TCP的特点是面向连接,就由这里的listen来体现。

1.5.1 关于listen() 函数的核心作用

listen() 是专属于TCP 服务器端 的系统调用,它的核心作用是:

将一个已经绑定(bind())了 IP 和端口的套接字(sockfd),从"主动套接字"转为"被动监听套接字",并告诉操作系统内核:该套接字准备好接受客户端的连接请求了

简单来说,listen() 是服务器"开启监听"的关键一步,调用后服务器才会真正开始等待客户端的连接,没有这一步,accept() 调用会直接失败。

一句话 listen() 的核心意义就是明确告诉操作系统:这个服务器套接字已经完成了绑定(IP + 端口),现在正式 "就绪",可以开始接收客户端的连接请求了。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int listen(int sockfd, int backlog);
2. 参数详解
参数 含义与注意事项
sockfd 输入参数,必须是已经通过 socket() 创建、且通过 bind() 绑定了地址的 TCP 套接字描述符(SOCK_STREAM 类型)。
backlog 输入参数,核心是设置"未完成连接队列"+"已完成连接队列"的总长度上限(不同系统实现略有差异),具体见下文"工作原理"。
3. 返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。
4. 关键注意事项
  • listen() 仅"开启监听",不会阻塞 :调用后函数立即返回,不会等待客户端连接(阻塞发生在 accept() 阶段);
  • listen() 不处理连接,只准备接收:真正取出连接的是 accept()listen() 只是"搭好架子";
  • UDP 协议不支持 listen():UDP 是无连接协议,不需要监听,调用 listen() 会返回错误(EOPNOTSUPP)。
5. listen() 调用后的套接字状态变化

通过 netstat 命令可以直观看到变化:

  • 调用 bind() 后、listen() 前:套接字状态为 CLOSESYN_SENT(无监听);
  • 调用 listen() 后:套接字状态变为 LISTEN,表示已进入监听状态。

示例命令(查看 8080 端口):

bash 复制代码
netstat -anp | grep 8080
# 输出示例(LISTEN 状态表示监听成功):
# tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      12345/./server
6. 总结
  1. listen() 的核心是将绑定后的 TCP 套接字转为监听状态,让内核为其维护连接队列,是服务器接收连接的前提;
  2. backlog 参数主要控制已完成连接队列的长度,现代系统会自动调整,常用值 5/10/128 即可;
  3. 关键特性:listen() 非阻塞、仅用于 TCP 服务器、调用后套接字状态变为 LISTEN,且必须在 bind() 之后、accept() 之前调用。

初始化完事儿后运行服务器。

修改运行状态,创建socket_in结构体 peer(输出型的参数,目的是带回申请连接的客户端)

调用 accept方法,这里会阻塞等待客户端申请连接。

关于accept

1.5.2 accept() 函数的核心作用

accept() 是专属于 TCP 服务器端 的系统调用,它的核心作用是:

从监听套接字(listen() 后的套接字)的"已完成连接队列"中,取出第一个完成三次握手的客户端连接,创建并返回一个全新的"通信套接字" ------这个新套接字专门用于和该客户端收发数据,而原监听套接字会继续保持 LISTEN 状态,等待其他客户端的连接。

简单来说,accept() 是服务器"接起电话"的动作:监听套接字是"总机"(一直等来电),accept() 是总机接线员,接起一个来电后,会分机到一个新的"通信套接字"(专门和这个客户通话),总机继续等下一个来电。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2. 参数详解
参数 类型 含义与使用注意事项
sockfd 输入参数 必须是已调用 socket()→bind()→listen() 的监听套接字描述符(状态为 LISTEN)。
addr 输出参数 指向 struct sockaddr 类型的指针,用于存储连接成功的客户端的 IP 地址和端口信息 ;若不关心客户端信息,可传 NULL
addrlen 输入/输出 指向 socklen_t 变量的指针: ① 调用前:赋值为 addr 结构体的长度(如 sizeof(struct sockaddr_in)); ② 调用后:内核会修改为实际存储的客户端地址长度。 若 addrNULL,此参数也传 NULL
3. 返回值
  • 成功 :返回一个非负的新套接字描述符 (通信套接字,状态为 ESTABLISHED);
  • 失败 :返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。
4. 关键特性:阻塞/非阻塞行为

accept() 的阻塞性由监听套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):

阻塞模式(默认)
  • 若"已完成连接队列"为空,accept()暂停程序执行(阻塞),直到队列中有新连接;
  • 这是最常用的模式,适合简单的单客户端服务器。
非阻塞模式
  • 需先通过 fcntl() 将监听套接字设为非阻塞:

    c 复制代码
    #include <fcntl.h>
    // 将listen_fd设为非阻塞
    int flags = fcntl(listen_fd, F_GETFL, 0);
    fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);
  • 若"已完成连接队列"为空,accept()立即返回 -1 ,并设置 errno = EAGAINEWOULDBLOCK

  • 适合高并发服务器(结合 select/epoll 实现多路复用)。

5. 常见错误与注意事项
1. 高频错误原因
errno 错误码 含义 常见场景
EINVAL 参数无效 监听套接字未调用 listen() 就调用 accept()
EBADF 文件描述符无效 sockfd 不是合法的套接字描述符;
EAGAIN/EWOULDBLOCK 非阻塞模式下无连接 非阻塞 accept() 调用时,已完成队列为空;
EINTR 调用被信号中断 阻塞 accept() 时,程序收到信号(如 Ctrl+C);
2. 重要注意事项
  • 两个套接字的区分accept() 返回的 conn_fd 是通信套接字,原 listen_fd 是监听套接字,二者独立,关闭 conn_fd 不影响 listen_fd
  • 监听套接字状态不变accept() 成功后,listen_fd 仍为 LISTEN 状态,可继续调用 accept() 接收新客户端;
  • 客户端信息的字节序client_addr.sin_addr(IP)和 client_addr.sin_port(端口)都是网络字节序 ,需用 inet_ntop()/ntohs() 转为主机字节序的字符串/数字。
6. 总结
  1. accept() 的核心是从已完成连接队列取出连接,创建新的通信套接字 ,原监听套接字保持 LISTEN 状态;
  2. 参数中 addr/addrlen 是输出参数,用于获取客户端信息,不关心可传 NULL
  3. 阻塞性由监听套接字类型决定:默认阻塞(等连接),非阻塞模式下无连接会立即返回错误;
  4. 关键区分:三次握手由内核自动完成,accept() 仅负责"取出"已建立的连接,不参与握手过程。

emmm...我的理解就是:调用accept本质上就是创建了一个新的套接字并且新套接字的状态是已经连接,作用就是和客户端进行通信,并且原先监听套接字并不改变,监听套接字的作用就只是监听和维护已经建立连接的队列。所以这里也就能理解为啥服务端类的成员变量是监听套接字。


ok,这里理一下思路

首先更改服务器运行状态,创建peer结构体(作用是带回连接的套接字),算一下peer类型的大小(等下特定服务里面有用),创建新的套接字描述符(文件描述符),调用accept函数(参数含义:和那个监听套接字里的队列连接?强制类型转化一下带回的参数,长度),判断是否连接成功(如果没有则跳过本次连接),(连接成功)创建InetAddr对象(用于输出日志信息),输出日志信息(让服务器打印日志,知道和谁建立了连接),调用指定服务(参数:服务器和客户端套接字信息)。

调用到service。明确我们的目标是接收客户端传来的信息,然后再回显。

创建缓冲区,读取数据(这里用的就是read函数,因为是面向字节流的)。关于read函数的参数(从哪读,读到哪,读的大小)

接着判断是否读取成功。然后用日志格式化一下要输出的信息,最后用write函数输出(依旧是因为write函数是面向字节流的)参数的意义:写到哪,写什么,写多少。

这里服务端就设计完了,接着看客户端:

指定调用格式,调用的时候要说清楚服务器的IP和端口。

创建客户端的sockfd,判断是否创建成功。

创建InetAddr对象,方便做格式的转化。

调用connect方法

关于connect方法

1.5.3 connect() 函数的核心作用

connect() 是专属于 TCP 客户端 的系统调用,它的核心作用是:

让客户端套接字主动向服务器端的监听套接字(LISTEN 状态)发起 TCP 连接请求,触发并完成三次握手,最终建立客户端与服务器之间的双向通信链路。

简单来说,connect() 是客户端"主动打电话给服务器"的动作------服务器的监听套接字是"总机",客户端通过 connect() 拨打这个总机号码,完成三次握手后,服务器的 accept() 会"接起电话"并分配通信套接字,双方就能开始对话。

1. 标准原型
c 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2. 参数详解
参数 类型 含义与使用注意事项
sockfd 输入参数 客户端通过 socket() 创建的 TCP 套接字描述符(SOCK_STREAM 类型),无需提前 bind()(内核会自动分配临时端口)。
addr 输入参数 指向 struct sockaddr 类型的指针,存储服务器的 IP 地址和端口信息 (必须是服务器已绑定、且处于 LISTEN 状态的地址)。
addrlen 输入参数 addr 结构体的长度(如 sizeof(struct sockaddr_in))。
3. 返回值
  • 成功 :返回 0,表示三次握手完成,客户端套接字进入 ESTABLISHED(已建立)状态;
  • 失败 :返回 -1,并设置全局变量 errno 表示错误原因(可通过 perror() 打印)。

关键细节:

  1. connect() 的阻塞/返回时机:默认情况下,connect() 会阻塞到三次握手完全完成(客户端发送 ACK 后)才返回 0;
  2. 客户端无需提前 bind():调用 socket() 后可直接 connect(),内核会自动为客户端分配本机 IP 和临时端口(如 54321);
  3. 三次握手是内核自动完成的:connect() 只是触发这个流程,无需开发者干预。
4. 关键特性:阻塞/非阻塞行为

connect() 的阻塞性由客户端套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):

阻塞模式(默认)
  • 调用后阻塞,直到三次握手完成(返回 0)或超时/失败(返回 -1);
  • 超时时间由内核决定(Linux 下约 75 秒),超时后 errno 设为 ETIMEDOUT
  • 这是最常用的模式,适合简单客户端。
非阻塞模式
  • 需先将客户端套接字设为非阻塞:

    c 复制代码
    #include <fcntl.h>
    int flags = fcntl(client_fd, F_GETFL, 0);
    fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
  • 调用 connect() 后会立即返回 -1 ,但 errno 设为 EINPROGRESS(表示三次握手正在进行中);

  • 需通过 select()/epoll() 监听套接字的"可写事件",来判断三次握手是否完成;

  • 适合高并发客户端(如同时连接多个服务器)。

5. 常见错误与注意事项
1. 高频错误原因
errno 错误码 含义 常见场景
ECONNREFUSED 连接被拒绝 服务器未启动、端口错误,或服务器未调用 listen()
ETIMEDOUT 连接超时 服务器不可达(如网络不通),或三次握手超时;
EINPROGRESS 连接正在进行中 非阻塞模式下 connect() 立即返回的正常状态;
EADDRINUSE 地址已被使用 客户端手动 bind() 了已占用的端口;
EHOSTUNREACH 主机不可达 服务器 IP 错误,或路由不通;
2. 重要注意事项
  • 客户端无需 bind() :除非需要固定客户端端口,否则直接 socket()→connect() 即可,内核会自动分配临时端口;
  • 连接失败后需重新创建套接字 :若 connect() 失败(如超时),该套接字无法再次用于 connect(),需关闭后重新 socket()
  • UDP 不支持 connect()(但可调用) :UDP 是无连接协议,调用 connect() 仅会记录服务器地址,不会建立连接,后续 send() 无需指定地址。
6. 总结
  1. connect() 是客户端主动发起 TCP 连接的核心接口,触发并等待三次握手完成,成功后客户端套接字进入 ESTABLISHED 状态;
  2. 客户端套接字无需提前 bind(),内核会自动分配 IP 和临时端口;
  3. 阻塞模式下 connect() 等待三次握手完成,非阻塞模式需通过 select()/epoll() 监听连接状态;
  4. 核心交互逻辑:客户端 connect() 触发三次握手 → 服务器内核将连接放入 ACCEPT 队列 → 服务器 accept() 取出连接,双方开始通信。

理解 connect() 的关键是抓住"客户端主动发起三次握手"这个核心,它和服务器的 accept() 是一一对应的------connect() 是"拨打电话",accept() 是"接起电话"。

我的理解:调用connect方法(谁要连接?连接谁?连接服务器的长度?)

判断是否连接成功

执行向服务器写逻辑:创建字符转用于接受输入,使用getline函数接受来自标准输入的参数,房子line中。

调用write函数,写到客户端的文件描述符中,指定写的内容,写的大小。

回显逻辑:创建缓冲区,调用read函数(从哪读?读到哪?读多少?)

判断是否读取成功。将读取到的buffer中的内容输出到屏幕。最后关闭文件。

Q:有点迷的是为啥也从socket里面读?这里的socket不是客户端的套接字吗?

A:先回答问题:因为服务器写到了socket里面,socket是客户端的套接字。

其实整个代码的执行逻辑是:服务器先运行,创建了监听套接字,不断地监听,并且也已经调用了accept方法,服务器运行到这里是不断阻塞的,就等待客户端的连接。随后客户端建立连接(客户端已经创建好了套接字),然后服务器就接收到了客户端的连接,并且创建了一个套接字用来接受accept方法的返回值。这里创建的套接字是共给服务器使用的,随后调用服务(传参就是服务器的套接字和客户端的套接字)

**至此第一版的demo已经设计完毕。**第一版的缺陷是,只允许单进程访问服务器,这显然不符合我们的常理对么?所以第二版引入多进程。

demo1 v2

相较于第一版,第二版只需要改一下服务端的逻辑
server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 回收子进程做法1. 
        // signal(SIGCHLD, SIG_IGN); // 父进程忽略子进程信号,让内核帮着回收子进程
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }
    
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            // Service(sockfd, addr);
            pid_t id = fork();
            if(id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if(id == 0) // 子进程
            {
                close(_listensockfd); // 关掉不想让子进程访问的套接字,关闭文件描述符
                // 回收子进程方法2:
                if(fork() > 0) // 子进程再次创建子进程,也就是让孙子进程去执行业务
                    exit(OK);

                Service(sockfd, addr); //孙子进程处理业务,孙子进程变成了孙子进程,被1号进程领养,被系统回收
                exit(OK);
            }
            else
            {
                close(sockfd); // 父进程管监听就行,不用管其他的套接字。
                pid_t rid = waitpid(id, nullptr, 0); // 回收子进程,采用方法2已经不会再阻塞,因为子进程瞬间退出了
                (void)rid;
            }
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

解释

思想:在执行业务之前,创建子进程,让子进程去执行业务,父进程持续的监听客户端。如此就能实现多进程访问服务器。根本原因是:父子进程并发运行。

对于子进程来说,是不要和监听作用隔离的,也就是不想让子进程访问监听套接字,所以关闭对应的文件描述符。

然后去执行对应的业务。

这里的问题是父进程要等待子进程,我们知道,父进程等待子进程是阻塞等待,如果阻塞等待的话,就有变成了串行,还是解决不了多客户端访问的问题。

对于这个问题有两个解决办法:

  1. 忽略子进程的信号
  2. 创建孙子进程
    对于方法二,注释已经写的很清楚,再次不多赘述。

到此就完成了demo的第二版,多进程访问服务器。

下面第三版是多线程访问服务器。

demo1 v3

server.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
    {
    }

    void Init()
    {
        // 1. 创建套接字,socket类比open
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;

        // 2. bind端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bing error";
            exit(BIND_ERR);
        }
        LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;

        // 3. 设置socket状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
    }

    class ThreadData
    {
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
        {
        }
        int sockfd;
        InetAddr addr;
        TcpServer *tsvr;
    };

    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // 1. 先读取数据
            ssize_t n = read(sockfd, buffer, sizeof(buffer - 1));
            if (n > 0)
            {
                // 给buffer设置成C风格的字符串
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
                // 2. 写回数据
                std::string echo_string = buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd);
                break;
            }
        }
    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();

            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);
        }
        _isrunning = false;
    }

    ~TcpServer() {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

demo2

该版本继续对上面升级,使用多线程执行具体的业务(远程执行命令行)。

点我查看源代码^ _ ^


相关推荐
猪脚踏浪1 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠17 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush417 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52018 小时前
Linux 11 动态监控指令top
linux
网络研究院19 小时前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智19 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest19 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩19 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_19 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈19 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix