从零入门计算机网络系列(六):TCP套接字编程实战——从listen监听到客户端与服务器通信

Hello大家好,上一期我们把UDP套接字编程的细节拆了个遍。这一篇是 Linux 网络编程系列的第六篇,主角换成面向连接的TCP,重点拆解TCP套接字编程的基础流程与核心API。

文章会先从socket()创建套接字、bind()绑定地址讲起,把参数含义和初始化规范一条条说清楚;再拿UDP做参照,重点聊TCP独有的listen()、监听状态(LISTEN),以及怎么用 netstat 这类工具查看连接状态;最后用一个"鱼庄拉客"的生活类比分,把accept()的工作原理掰开揉碎,讲明白"监听套接字"和"服务套接字"到底怎么分工,顺便看看单进程TCP服务器的瓶颈卡在哪里。

好的,我们开始吧。

目录

一、TCP套接字API详解:从创建套接字到建立连接

[1.1 创建套接字:socket函数](#1.1 创建套接字:socket函数)

[1.2 绑定本地地址:bind函数](#1.2 绑定本地地址:bind函数)

[1.3 开启监听:listen函数与TCP监听状态](#1.3 开启监听:listen函数与TCP监听状态)

[1.3.1 listen函数详解:参数与监听队列](#1.3.1 listen函数详解:参数与监听队列)

[1.3.2 查看网络进程的监听状态](#1.3.2 查看网络进程的监听状态)

[1.3.3 TCP连接建立过程与状态转换](#1.3.3 TCP连接建立过程与状态转换)

[1.4 接受连接:accept函数与套接字的分工](#1.4 接受连接:accept函数与套接字的分工)

[1.4.1 从"好再来鱼庄"的故事理解监听与接待](#1.4.1 从“好再来鱼庄”的故事理解监听与接待)

[1.4.2 accept函数详解:获取已建立的连接](#1.4.2 accept函数详解:获取已建立的连接)

[1.4.3 连接建立后的数据读取与写入](#1.4.3 连接建立后的数据读取与写入)

[1.5 客户端如何连接服务器:connect函数](#1.5 客户端如何连接服务器:connect函数)

[1.5.1 connect函数的基本用法](#1.5.1 connect函数的基本用法)

[1.5.2 connect函数的使用示例](#1.5.2 connect函数的使用示例)

[1.5.3 connect函数背后的TCP连接建立过程](#1.5.3 connect函数背后的TCP连接建立过程)

[1.5.4 connect、listen与accept之间的关系](#1.5.4 connect、listen与accept之间的关系)

[1.5.5 客户端需要提前调用bind吗?](#1.5.5 客户端需要提前调用bind吗?)

二、完整源码与运行测试

[2.1 InetAddr.hpp(IP 地址封装类)](#2.1 InetAddr.hpp(IP 地址封装类))

[2.2 TcpServer.hpp(TCP 服务器类)](#2.2 TcpServer.hpp(TCP 服务器类))

[2.3 TcpServer.cc(服务端主程序)](#2.3 TcpServer.cc(服务端主程序))

[2.4 TcpClient.cc(客户端主程序)](#2.4 TcpClient.cc(客户端主程序))

[2.5 Makefile](#2.5 Makefile)

[2.6 运行方式](#2.6 运行方式)


一、TCP套接字API详解:从创建套接字到建立连接

搞TCP网络编程,第一件事得把Linux提供的标准套接字接口摸熟。这些接口都定义在<sys/socket.h>头文件里。跟UDP那边一样,TCP通信的第一步也是先创建一个套接字,把通信端点立起来。

1.1 创建套接字:socket函数

socket()函数干的事,就是打开一个网络通信端口。调用成功的话,它跟open()系统调用一个套路,返回一个套接字文件描述符(Socket Descriptor)。后面所有的网络读写操作,都围着这个描述符转。函数声明:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数与返回值,跟UDP基本一个模子。唯一的区别在type:指定数据传输的服务类型。TCP这边,type必须写成SOCK_STREAM,表示面向连接的、可靠的字节流传输协议。

1.2 绑定本地地址:bind函数

套接字创建出来了,还只是个空壳子,没跟任何地址挂上钩。bind()的活,就是把这个套接字跟本地的IP地址和端口号绑在一起。函数声明:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

三个参数,逐个拆:

  • sockfd:socket()返回的那个套接字文件描述符,告诉函数要绑哪个套接字。
  • addr:指向本地地址结构体的指针。实际填的是struct sockaddr_in,传进去的时候强转成通用的struct sockaddr*。
  • addrlen:地址结构体的大小,传sizeof(struct sockaddr_in) 就行。

返回值:成功返回 0,失败返回 -1,并设置 errno。

1.3 开启监听:listen函数与TCP监听状态

UDP是无连接的,发出去就不管了,压根没有"等谁来连"这回事。TCP不一样,它是面向连接的,连接没建立起来,数据就没法传。

打个餐馆的比方。你去吃饭,老板一定守在店里,随时等着客人上门。客人没来,他也守着;客人来了一拨又一拨,他继续守着。这种"一直等在门口"的姿态,就是监听。

邮件是另一回事。你发一封邮件,收件人不需要坐在电脑前一直等,邮件先躺在服务器上,他什么时候登录什么时候取。发件人和收件人,根本不用同时在线。这就是面向数据报的路子。

TCP走的是餐馆模式:服务端得先摆好架势,进入监听状态,等着客户端来连。连接没来,它就守着;连接来了,它才起身接客。所以listen()这个函数,UDP那边压根用不上,TCP这边却绕不开。

1.3.1 listen函数详解:参数与监听队列

listen()干的事,一句话:让sockfd进入监听状态,摆好架势,等着客户端上门。

门口不可能只来一个人。多个客户端同时发起连接请求,这些请求就得排队等着处理。listen()的第二个参数,管的就是这条队最多能排多长。函数签名:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数与返回值,逐个拆:

  • sockfd:已经用bind()绑好地址和端口号的套接字文件描述符。地址都没绑,监听个啥?所以bind一定得在listen前面。
  • backlog:提示队列的最大排队连接数。说白了,就是"门口最多允许多少人排队等"。最多允许backlog个客户端处于连接等待状态,超出的连接请求,系统直接忽略,连排队的机会都不给。这个值通常不会设得太大,5、128都是常见的选择。设太大,队列冗长,处理不过来反而添乱;设太小,又容易把正常请求挡在门外。

返回值:成功返回0,失败返回-1。

1.3.2 查看网络进程的监听状态

只要TCP服务器成功调用了listen(),进入LISTEN状态,它就已经具备了接受客户端连接的能力,门开了,就等人来。想看系统里哪些套接字正在监听,Linux命令行工具就能搞定:

bash 复制代码
# 查看所有 TCP 和 UDP 监听及连接状态(显示 PID 及程序名)
netstat -tnap

# 仅查看处于 LISTEN 状态的套接字
netstat -tnlp

常用参数,逐个认一下:

  • -t**(tcp)**:只看TCP相关的套接字。

  • -u**(udp)**:只看UDP相关的套接字。

  • -n(numeric):直接显示数字形式的IP和端口,不做域名和服务名解析。速度快,看着也清爽。

  • -l**(listening)**:只揪出处于LISTEN状态的套接字。想看谁在守店,加它就对了。

  • -p**(programs)**:显示进程名称,顺带把PID也亮出来。谁在监听,一查便知。

1.3.3 TCP连接建立过程与状态转换

拿telnet工具向服务器发起连接,整个状态变化过程一目了然。

未连接之前,服务器进程就守在LISTEN状态,安安静静等客上门:

bash 复制代码
Proto Recv-Q Send-Q Local Address    Foreign Address   State       PID/Program name
tcp      0      0    0.0.0.0:8080     0.0.0.0:*         LISTEN      1080682/./tcpserver

客户端执行telnet 127.0.0.1 8080建立连接之后,网络状态里立刻多出对应的 ESTABLISHED 记录,表示连接已经成功建立:

bash 复制代码
Proto Recv-Q Send-Q Local Address    Foreign Address   State       PID/Program name
tcp      0      0    0.0.0.0:8080     0.0.0.0:*         LISTEN      1080682/./tcpserver
tcp      0      0    127.0.0.1:58752  127.0.0.1:8080    ESTABLISHED 1080746/telnet
tcp      0      0    127.0.0.1:8080   127.0.0.1:58752   ESTABLISHED -

仔细看,同一台主机上测试的时候,netstat输出里冒出了两条ESTABLISHED记录。为什么?

第一,TCP是全双工通信。 通信双方既是发送方,也是接收方,两条方向相反的数据通道各自独立存在。一条连接,两头各有一条链路,系统两边都得记着。

**第二,同机测试。**客户端和服务器部署在同一台机器上,系统在回环网卡(127.0.0.1)上,同时记录了"客户端端口 → 服务器端口"和"服务器端口 → 客户端端口"这两条对称链路。所以看起来像是两条连接,实际上是同一条TCP连接的两端,各自记了一笔。

1.4 接受连接:accept函数与套接字的分工

学TCP套接字,最容易让人犯迷糊的,莫过于accept()的返回值,为什么调用它之后,系统又甩给你一个全新的套接字文件描述符?原来创建的那个套接字,又跑哪儿去了?

1.4.1 从"好再来鱼庄"的故事理解监听与接待

假设西湖边开着一家极其火爆的"好再来鱼庄"。

拉客员(张三):站在鱼庄门口、马路边,专门负责热情招呼路过的游客。他的工作非常纯粹,只要看到有顾客想用餐,就把人引导进店,交给店内的服务员。点菜、端茶、倒水,一概不归他管。引导完一位客人,他立刻回到门口,继续拉客。

服务员(李四、王二麻):坐在店内,专门对接已经被张三带进来的顾客。点菜、上菜、陪聊、结账,具体的服务工作全由他们负责。

这个故事里,两个角色对应得清清楚楚:

  • **监听套接字(listensockfd)就是拉客员张三。**它唯一的使命,是在特定端口上监听外部请求,负责建立并"获取"连接,不承担后续与客户端的数据通信(I/O)。拉完客,它扭头就走,回门口继续守着。

  • **服务套接字(accept()返回的sockfd)就是服务员李四。**连接建立之后,系统专门分配这个套接字,来跟该具体客户端进行read/write数据交互。接待、服务、收尾,全是它的活儿。

1.4.2 accept函数详解:获取已建立的连接

这里有个认知必须先摆正:TCP三次握手建立连接的过程,完全由操作系统内核自动完成,跟有没有调用accept()没有半点关系。 连接在accept()之前就已经建好了,安安静静躺在内核的已完成连接队列里。accept()的本质作用,是从这个队列里取走一个已经建立好的连接。

函数签名:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数与返回值,逐个拆:

  • sockfd:监听套接字描述符,也就是listensockfd,对应拉客员张三。它专门传进accept()用于获取连接。

  • addr:输出型参数,用来获取连接成功的客户端网络地址信息,包含客户端的IP和端口号。谁来了,一查便知。

  • addrlen:值-结果参数。传进去时,是addr结构体的长度;函数返回时,变成实际写入的地址结构体大小。

  • 返回值 :成功时,返回一个全新的套接字文件描述符,服务套接字,对应店里的服务员。获取失败,返回-1。

后续针对该客户端的所有数据传输,read/write,或者recv/send,统统使用accept()返回的这个新套接字来操作。拉客员只负责把人带进来,端茶倒水、点菜结账,是服务员的活儿。

1.4.3 连接建立后的数据读取与写入

收发数据这一块,TCP跟UDP走的是两条路。

UDP面向数据报,有自己专门的收发接口recvfrom/sendto,每个数据包独立处理,边界清清楚楚。

TCP面向字节流,没有"包"的概念,数据就是一条连续的水流。所以它不需要什么专用接口,直接用标准的Linux文件I/O系统调用read()和write()就行。套接字在Linux里本来就是个文件描述符,读写它,跟读写普通文件是一个道理。水流进来,水流出去,字节连着字节,边界得由应用层自己去划。

1.5 客户端如何连接服务器:connect函数

前面我们已经介绍了TCP服务器端的基本流程:通过socket()创建套接字,使用bind()绑定本地地址,再通过listen()进入监听状态,最后调用accept()获取客户端连接。

那么问题来了:服务器已经准备好了,客户端应该如何主动发起连接呢?

答案就是connect() 函数。connect()是TCP客户端编程中的核心接口,它的主要作用是主动向指定的服务器地址发起连接请求。当连接建立成功后,客户端就可以通过这个套接字进行数据的发送与接收。

1.5.1 connect函数的基本用法

首先来看connect()的函数原型:

cpp 复制代码
#include <sys/socket.h>

int connect(int sockfd,
            const struct sockaddr *addr,
            socklen_t addrlen);

这个函数有三个参数:

  • sockfd: 由 socket() 创建的套接字文件描述符,表示客户端准备使用哪个套接字建立连接。

  • addr: 指向服务器地址结构体的指针,用于指定要连接的目标IP地址和端口号。

  • addrlen: 地址结构体的长度,通常通过sizeof()获取。

返回值也比较简单:

  • 返回0: 连接建立成功。

  • 返回-1: 连接失败,同时设置errno,可以通过perror()等方式查看错误信息。

1.5.2 connect函数的使用示例

在调用connect()之前,我们需要先创建TCP套接字,并准备好服务器的地址信息。

例如,假设服务器监听的IP地址为127.0.0.1,端口号为8080,客户端可以这样编写:

cpp 复制代码
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstdio>

int main()
{
    // 1. 创建 TCP 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    // 2. 填充服务器地址
    struct sockaddr_in server_addr{};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);

    if (inet_pton(AF_INET, "127.0.0.1",
                  &server_addr.sin_addr) != 1)
    {
        fprintf(stderr, "IP地址格式无效\n");
        close(sockfd);
        return 1;
    }

    // 3. 主动发起连接
    if (connect(sockfd,
                (struct sockaddr*)&server_addr,
                sizeof(server_addr)) == -1)
    {
        perror("connect");
        close(sockfd);
        return 1;
    }

    printf("连接服务器成功!\n");

    // 4. 后续可以通过sockfd进行数据收发
    // send(sockfd, ...);
    // recv(sockfd, ...);

    close(sockfd);
    return 0;
}

这段代码的执行过程可以分成四步:

  1. 调用socket()创建一个TCP套接字。

  2. 填写服务器的IP地址与端口号,并完成必要的网络字节序转换。

  3. 调用connect(),主动请求与服务器建立连接。

  4. 连接成功后,使用同一个sockfd进行后续数据收发。

1.5.3 connect函数背后的TCP连接建立过程

调用connect()并不意味着客户端只是简单地记录一个服务器地址。对于TCP而言,它会触发真正的连接建立过程。

在正常的TCP三次握手过程中,通信双方需要完成以下步骤:

  1. 第一次握手: 客户端向服务器发送SYN,请求建立连接。

  2. 第二次握手: 服务器收到请求后,返回SYN + ACK,表示确认客户端的请求,同时也请求建立连接。

  3. 第三次握手: 客户端收到响应后,再发送ACK,双方完成连接建立。

当连接成功建立后,客户端的connect()返回0,后续便可以通过该套接字进行TCP数据通信。

此时,服务端的accept()负责从已完成连接的队列中取出连接,并返回一个新的套接字文件描述符,供服务端与该客户端进行通信。

这里有一个容易混淆的细节:客户端的connect()成功返回,并不意味着服务端应用程序已经执行了accept()。 TCP握手可以先由内核完成,已建立的连接随后等待服务端程序调用 accept() 取出。

1.5.4 connect、listen与accept之间的关系

将客户端和服务端的流程放在一起,就更容易理解这三个接口的分工。

接口 所在一侧 核心作用
connect() 客户端 主动发起连接请求
listen() 服务端 将TCP套接字置于监听状态,准备接收连接
accept() 服务端 获取一个已建立的连接,并返回用于通信的新套接字

可以将这个过程概括为:

客户端调用connect()发起连接,服务端通过listen()等待连接,并调用accept()获取连接。

还要注意,服务端的监听套接字与accept()返回的已连接套接字并不是同一个套接字。监听套接字继续负责接收后续连接,而新返回的套接字负责与某个客户端进行数据通信。

因此,服务器可以在保留监听能力的同时,与已经连接的客户端交换数据。

1.5.5 客户端需要提前调用bind吗?

前面介绍服务器端时,我们专门讲过bind()函数。那么客户端调用connect()之前,是否也必须通过bind()指定自己的本地IP地址和端口号呢?

通常不需要。

如果客户端没有显式调用bind(),操作系统一般会在连接过程中自动选择合适的本地IP地址和一个可用的临时端口号。客户端只需要指定目标服务器的地址,就可以发起连接。

当然,如果业务确实要求客户端使用指定的本地地址或端口,也可以提前调用bind()。

这样就能理解,服务器通常需要显式绑定固定端口,以便客户端能够按照约定访问;而普通客户端一般不需要固定本地端口,因为操作系统可以自动分配。

最后还需要区分阻塞与非阻塞模式:对于默认的阻塞套接字,connect()通常会等待连接建立成功或失败;对于非阻塞套接字,如果连接暂时无法立即完成,函数通常会返回-1,并将errno设置为 EINPROGRESS,此时需要通过poll()、select()等机制继续判断连接结果。

二、完整源码与运行测试

下面把TCP服务器与客户端的完整代码整理出来,包括IP地址封装类、TCP服务器类、服务端主程序、客户端主程序,以及Makefile。代码可直接编译运行。

2.1 InetAddr.hpp(IP 地址封装类)
cpp 复制代码
#pragma once

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string>
#include <cstring>

namespace NetModule
{
    class InetAddr
    {
    public:
        // 从 sockaddr_in 结构提取 IP 和端口(服务端获取客户端地址时使用)
        InetAddr(const struct sockaddr_in &addr)
            : _addr(addr)
        {
            _port = ntohs(_addr.sin_port);

            char ipbuffer[64];
            inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
            _ip = ipbuffer;
        }

        // 从 IP 和 Port 构建 sockaddr_in 结构(客户端指定服务端地址时使用)
        InetAddr(const std::string &ip, uint16_t port)
            : _ip(ip), _port(port)
        {
            memset(&_addr, 0, sizeof(_addr));
            _addr.sin_family = AF_INET;
            _addr.sin_port = htons(_port);
            inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        }

        std::string Ip() const { return _ip; }
        uint16_t Port() const { return _port; }
        struct sockaddr_in GetAddr() const { return _addr; }

        ~InetAddr() {}

    private:
        struct sockaddr_in _addr;
        std::string _ip;
        uint16_t _port;
    };
}
2.2 TcpServer.hpp(TCP 服务器类)
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#include "InetAddr.hpp"

namespace NetModule
{
    // 业务处理函数类型:输入请求字符串,返回响应字符串
    using func_t = std::function<std::string(const std::string &)>;

    const uint16_t DEFAULT_PORT = 8080;
    const int BACKLOG = 5;

    class TcpServer
    {
    public:
        TcpServer(uint16_t port = DEFAULT_PORT, func_t func = nullptr)
            : _port(port), _func(func), _isrunning(false), _listensockfd(-1)
        {
        }

        // 初始化:创建套接字 + 绑定地址 + 进入监听状态
        void Init()
        {
            // 1. 创建 TCP 套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensockfd < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(1);
            }
            std::cout << "socket create success, listensockfd: " << _listensockfd << std::endl;

            // 2. 设置端口复用,避免服务器重启时 bind 失败
            int opt = 1;
            setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

            // 3. 填充本地地址结构体
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;

            // 4. 绑定地址
            int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
            if (n < 0)
            {
                std::cerr << "bind error" << std::endl;
                exit(2);
            }
            std::cout << "bind success, port: " << _port << std::endl;

            // 5. 进入监听状态
            n = listen(_listensockfd, BACKLOG);
            if (n < 0)
            {
                std::cerr << "listen error" << std::endl;
                exit(3);
            }
            std::cout << "listen success" << std::endl;
        }

        // 启动服务器:循环 accept + 读写
        void Start()
        {
            _isrunning = true;

            // 忽略 SIGCHLD,避免子进程变成僵尸
            signal(SIGCHLD, SIG_IGN);

            while (_isrunning)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);

                // 1. 获取一个已建立好的连接
                int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
                if (sockfd < 0)
                {
                    std::cerr << "accept error" << std::endl;
                    continue;
                }

                InetAddr peerAddr(peer);
                std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
                          << "]# 连接建立, 服务套接字: " << sockfd << std::endl;

                // 2. 创建子进程处理该客户端
                pid_t id = fork();
                if (id == 0)
                {
                    close(_listensockfd);   // 子进程不需要监听套接字
                    Service(sockfd, peerAddr);
                    close(sockfd);
                    exit(0);
                }
                close(sockfd);              // 父进程不需要服务套接字
            }
        }

        ~TcpServer()
        {
            if (_listensockfd >= 0)
            {
                close(_listensockfd);
            }
        }

    private:
        // 与单个客户端通信
        void Service(int sockfd, const InetAddr &peerAddr)
        {
            char buffer[1024];

            while (true)
            {
                ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
                if (s > 0)
                {
                    buffer[s] = 0;
                    std::string request = buffer;
                    std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
                              << "]# " << request << std::endl;

                    std::string response;
                    if (_func)
                    {
                        response = _func(request);
                    }
                    else
                    {
                        response = "Server received: " + request;
                    }

                    write(sockfd, response.c_str(), response.size());
                }
                else if (s == 0)
                {
                    std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
                              << "]# 客户端断开连接" << std::endl;
                    break;
                }
                else
                {
                    std::cerr << "read error" << std::endl;
                    break;
                }
            }
        }

        int _listensockfd;
        uint16_t _port;
        bool _isrunning;
        func_t _func;
    };
}
2.3 TcpServer.cc(服务端主程序)
cpp 复制代码
#include "TcpServer.hpp"
#include <memory>

// 业务处理回调:把收到的字符串转成大写再返回
std::string Handler(const std::string &request)
{
    std::string response = request;
    for (auto &ch : response)
    {
        if (ch >= 'a' && ch <= 'z')
        {
            ch -= 32;
        }
    }
    return response;
}

// 用法:./tcp_server [端口号]
int main(int argc, char *argv[])
{
    uint16_t port = NetModule::DEFAULT_PORT;

    if (argc == 2)
    {
        port = static_cast<uint16_t>(std::stoi(argv[1]));
    }

    std::unique_ptr<NetModule::TcpServer> server =
        std::make_unique<NetModule::TcpServer>(port, Handler);

    server->Init();
    server->Start();

    return 0;
}
2.4 TcpClient.cc(客户端主程序)
cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 用法:./tcp_client [服务端IP] [服务端端口]
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = static_cast<uint16_t>(std::stoi(argv[2]));

    // 1. 创建 TCP 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    // 2. 填充服务端地址结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    // 3. 向服务端发起连接(客户端不需要 bind)
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl;
        close(sockfd);
        return 3;
    }
    std::cout << "connect success" << std::endl;

    // 4. 循环发送 + 接收
    while (true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        if (line == "quit" || line == "exit")
        {
            break;
        }

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

        char buffer[1024];
        ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "Server Response# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "服务端已关闭连接" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

    close(sockfd);
    return 0;
}
2.5 Makefile
cpp 复制代码
.PHONY: all
all: tcp_server tcp_client

tcp_server: TcpServer.cc TcpServer.hpp InetAddr.hpp
	g++ -o $@ $< -std=c++17

tcp_client: TcpClient.cc
	g++ -o $@ $< -std=c++17

.PHONY: clean
clean:
	rm -f tcp_server tcp_client
2.6 运行方式

编译:

make

先启动服务端(默认端口8080):

./tcp_server

再启动客户端,指定服务端IP和端口:

./tcp_client 127.0.0.1 8080

客户端输入内容,服务端会收到并打印,同时把字符串转成大写返回给客户端。输入quit或exit退出客户端。


如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持。你的每一次反馈,都是我继续硬核输出的最大动力。我们下篇见。

相关推荐
2401_872418781 小时前
把 Ubuntu 服务器变成 AI Agent 终端:用自然语言或语音管理 Linux,任何命令都先审批(附完整 Python 源码)
linux·服务器·ubuntu·agent
coderSong25682 小时前
【Ubuntu】ctrl+shift+f 繁体中文切换、VScode全集搜索
linux·vscode·ubuntu
努力实干2 小时前
C++ 面向对象与工程架构
开发语言·c++·架构
AR-26710-2 小时前
Linux Day22——复习第三部分
linux·运维
迪康coolmu2 小时前
基于迪康端点安全一体化管理系统的公用终端上机与信创补丁实战
linux·运维·服务器·网络·安全·电脑
代钦塔拉2 小时前
【C++/Qt 进阶】设计模式系列(6):桥接模式——把“两个独立变化的维度“拆开
c++·qt·设计模式
blueSatchel2 小时前
imx6ull显示控制器设备驱动解读
linux·嵌入式
水饺编程2 小时前
第2章:Linux 基础,关机命令
linux·c语言·汇编
JiMoKuangXiangQu2 小时前
Linux CFS 负载均衡:从 CPU 拓扑到任务迁移
linux·负载均衡