一文读懂TCP通信机制:基于相关API构建可靠性连接

目录

前言

一、TCP通信

1、相关API

(1)socket

(2)bind

(3)listen

(4)accept

(5)connect

[2、Echo Server](#2、Echo Server)

(1)common

(2)mutex

(3)log

[<1> logstrategy](#<1> logstrategy)

[<2> consolelogstrategy](#<2> consolelogstrategy)

[<3> filelogstrategy](#<3> filelogstrategy)

[<4> loglevel](#<4> loglevel)

[<5> levelstr](#<5> levelstr)

[<6> gettimestamp](#<6> gettimestamp)

[<7> logmessage](#<7> logmessage)

[<8> logger](#<8> logger)

[<9> 全局实例与宏](#<9> 全局实例与宏)

(4)inetaddr

[<1> 构造函数](#<1> 构造函数)

[<2> 访问器](#<2> 访问器)

(5)tcpserver

[<1> 初始化](#<1> 初始化)

[<2> service](#<2> service)

[<3> run](#<3> run)

(6)tcpserver.cc

(7)tcpclient.cc

(8)测试

结语


前言

在Linux网络传输层协议中,UDP、TCP是两种比较经典的网络通信方式。与UDP的不可靠、无连接通信不同,客户端和服务端的TCP通信实现的是在不可靠的IP网络上构建一个可靠、有序、双向的字节流通道。TCP提供面向连接的可靠字节流传输服务,通信前需建立客户端和服务端的连接,通信结束后需释放连接,TCP的可靠性连接本质是调用相关API接口来实现,通过这些API接口将内核协议栈的工作搬到用户态完成,这些工作包括了客户端发起连接、服务端监听管理相关连接等,通过这些API接口来同时保障TCP连接的有序性、可靠性。本文将从TCP的相关API接口切入,并基于这些API接口手写实现TCP的可靠性通信。

一、TCP通信

1、相关API

下面介绍TCP通信中用到的相关API,这些API接口都在<sys/socket.h>头文件中。

(1)socket

socket用于打开一个网络通讯端口,如果调用成功,将返回一个文件描述符,若调用出错,则返回-1;应用程序可以像读写文件一样调用read/write在网络上收发数据;对于IPv4,domain参数指定为AF_INET;对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议;protocol参数一般指定为0。

(2)bind

服务器需要调用bind绑定一个固定的网络地址和端口号,客户端得知服务器的地址和端口号后就可以向服务器发起连接;服务器程序所监听的网络地址和端口号通常是固定不变的。

bind的作用是将参数socket和address绑定在一起,使socket这个用于网络通讯的文件描述符监听address所描述的地址和端口号;上一篇博客提过,struct sockaddr* 是一个通用指针类型,address参数实际上是可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen来指定结构体的长度;bind调用成功返回0,失败返回-1。

对address参数是这样来进行初始化的:

将整个结构体清零;设置地址类型为AF_INET;网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接才确定下来到底用哪个IP地址;SERV_PORT为用户指定绑定的端口号。

(3)listen

listen用于声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略;listen成功返回0,失败返回-1。

(4)accept

服务器通过调用accept接受连接,若没有客户端的连接请求,就阻塞等待直到有客户端连接上来;addr是一个传出参数,accept返回时传出客户端的地址和端口号,如果给addr参数传NULL,表示不关心客户端的地址;addrlen参数是一个传入传出参数,传入的是调用者提供的,缓冲区addr的长度为避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度。

服务器程序结构如下:

(5)connect

客户端需要调用connect连接服务器,connect和bind参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址,connect成功返回0,出错返回-1。

2、Echo Server

下面通过TCP通信实现一个简单的回显服务器,用户在客户端输入信息,服务器回显用户输入的信息,该回显服务器主要包含以下几个功能模块:

(1)common

common模块是一个基础设施模块,被该服务器项目的几乎所有其他模块包含,它不包含复杂的业务逻辑,用于提供网络编程必需的系统头文件、统一的错误码枚举、防拷贝基类、相关宏定义。

common.hpp

cpp 复制代码
#ifndef _COMMON_HPP_
#define _COMMON_HPP_
#include<iostream>
#include<functional>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
enum exitcode
{
    OK=0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};
class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy& )=delete;
    const nocopy& operator=(const nocopy& )=delete;
    ~nocopy(){}
};
#define CONV(addr) ((struct sockaddr*)&addr)
#endif

common.hpp集中包含了后续模块都会用到的标准库与系统头文件,exitcode枚举用于定义程序退出时的错误码,用于exit调用,枚举常量OK表示正常退出,USAGE_ERR为命令行参数错误,SOCKET_ERR表示socket调用失败,BIND_ERR为bind调用失败,LISTEN_ERR表示listen调用失败,CONNECT_ERR为客户端connect调用失败,枚举值从0开始自动递增,nocopy类的拷贝构造、赋值重载均被删除,禁止类的对象被拷贝,任何继承自nocopy的类,其拷贝构造和拷贝赋值都会被删除,后面实现的tcpserver类将继承该类,这是因为tcpserver类管理着监听套接字这样不可复制的资源,因此不允许拷贝。CONV宏用于将任意类型的地址结构指针转换为struct sockaddr*类型,便于后续进行类型转换。

(2)mutex

mutex模块是一个线程同步基础模块,主要用于配合日志模块实现多线程环境下的安全输出,实现一个基于RAII的互斥锁封装,为多线程环境提供线程同步机制,整个模块包含两个核心类:mutex、lockguard。

mutex.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
namespace mutexmodule
{
    class mutex
    {
    public:
        mutex()
        {
            pthread_mutex_init(&_mutex,nullptr);
        }
        void lock()
        {
            int n=pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void unlock()
        {
            int n=pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t* get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };
    class lockguard
    {
    public:
        lockguard(mutex& mutex):_mutex(mutex)
        {
            _mutex.lock();
        }
        ~lockguard()
        {
            _mutex.unlock();
        }
    private:
        mutex& _mutex;
    };
}

pthread_mutex_init(&_mutex,nullptr),构造函数mutex调用pthread_mutex_init完成互斥锁的初始化,lock函数通过调用pthread_mutex_lock来尝试获取锁,unlock函数调用pthread_mutex_unlock来释放当前线程持有的互斥锁,析构函数~mutex调用pthread_mutex_destroy来销毁互斥锁,释放相关资源,get用于返回底层pthread_mutex_t 指针。lockguard类为RAII锁守卫,构造函数在构造时调用lock自动加锁,析构函数在析构时调用unlock自动解锁。

(3)log

log模块用于实现一个线程安全、多级别、可扩展的日志系统,是整个项目最重要的模块之一,采取策略模式,将日志的输出方式与日志的生成逻辑解耦。

log.hpp

<1> logstrategy
cpp 复制代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include<iostream>
#include<string>
#include<cstdio>
#include<filesystem>
#include<fstream>
#include<sstream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"mutex.hpp"
using namespace std;
namespace logmodule
{
    using namespace mutexmodule;
    const string gsep ="\r\n";
    class logstrategy
    {
    public:
        virtual ~logstrategy()=default;
        virtual void synclog(const string& message)=0;
    };
}
#endif

logstrategy为日志策略抽象基类,采取策略实现模式,虚析构函数确保派生类对象被正确销毁,synclog虚函数用于定义日志输出接口,派生类必须实现。

<2> consolelogstrategy
cpp 复制代码
class consolelogstrategy:public logstrategy
{
public:
    consolelogstrategy(){}
    void synclog(const string& message) override
    {
        lockguard guard(_mutex);
        cout<<message<<gsep;
    }
    ~consolelogstrategy(){}
private:
    mutex _mutex;
};

consolelogstrategy为控制台日志输出策略,继承logstrategy基类,synclog使用mutex保证多线程环境下cout输出的原子性,lockguard实现自动加锁、解锁功能。

<3> filelogstrategy
cpp 复制代码
const string defaultpath="./log";
const string defaultfile="my.log";
class filelogstrategy:public logstrategy
{
public:
    filelogstrategy(const string& path=defaultpath,const string& file=defaultfile)
    :_path(path),
     _file(file)
    {
        lockguard guard(_mutex);
        if(filesystem::exists(_path))
        {
            return;
        }
        try
        {
            filesystem::create_directories(_path);
        }
        catch(const filesystem::filesystem_error& e)
        {
            cerr<<e.what()<<endl;
        }
     }
     void synclog(const string& message) override
     {
         lockguard guard(_mutex);
         string filename=_path+(_path.back()=='/'?"":"/")+_file;
         ofstream out(filename,ios::app);
         if(!out.is_open())
         {
             return;
         }
         out<<message<<gsep;
         out.close();
     }
     ~filelogstrategy(){}
private:
    string _path;
    string _file;
    mutex _mutex;
};

filelogstrategy为日志文件输出策略,默认日志目录为当前目录下的log文件夹,文件名为my.log,构造函数通过构造lockguard,来保证目录创建过程的线程安全,调用filesystem::exist(_path),检查日志目录是否存在,filesystem::create_directories(_path),通过递归创建目录,try、catch进行异常的捕获。synclog通过加锁保证多线程写入不交错,ofstream out(filename,ios::app),以追加模式打开文件,并写入日志,out.close(),最后关闭文件。

<4> loglevel
cpp 复制代码
enum class loglevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

loglevel为日志级别枚举,DEBUG为调试级别日志信息、INFO为一般信息、WARNING为警告信息、ERROR为错误级别日志、FATAL为致命错误。

<5> levelstr
cpp 复制代码
string levelstr(loglevel lev)
{
    switch(lev)
    {
        case loglevel::DEBUG: return "DEBUG";
        case loglevel::INFO: return "INFO";
        case loglevel::WARNING: return "WARNING";
        case loglevel::ERROR: return "ERROR";
        case loglevel::FATAL: return "FATAL";
        default: return "UNKNOWN";
    }
}

levelstr用于将日志级别转为字符串,将loglevel的枚举值转换为可读的字符串,用于日志输出。

<6> gettimestamp
cpp 复制代码
string gettimestamp()
{
    time_t t=time(nullptr);
    struct tm curr_tm;
    localtime_r(&t,&curr_tm);
    char timebuffer[128];
    snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
        curr_tm.tm_year+1900,
        curr_tm.tm_mon+1,
        curr_tm.tm_mday,
        curr_tm.tm_hour,
        curr_tm.tm_min,
        curr_tm.tm_sec
    );
    return timebuffer;
}

gettimestamp函数用于获取当前时间戳。

<7> logmessage
cpp 复制代码
class logmessage
{
public:
    logmessage(const string& src,loglevel level,int num,logger& logger)
    :_pid(getpid())
    ,_src(src)
    ,_num(num)
    ,_curr_time(gettimestamp())
    ,_level(level)
    ,_logger(logger)
    {
        stringstream ss;
        ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["           <<_src<<"]"<<"["<<_num<<"]"<<"-";
        _loginfo=ss.str();
    }
    template<class K>
    logmessage& operator<<(const K& info)
    {
        stringstream ss;
        ss<<info;
        _loginfo+=ss.str();
        return *this;
    }
    ~logmessage()
    {
        if(_logger._ptr)
        {
            _logger._ptr->synclog(_loginfo);
        }
    }
private:
    string _curr_time;
    loglevel _level;
    pid_t _pid;
    string _src;
    int _num;
    string _loginfo;
    logger& _logger;
};

logmessage构造时通过字符串流接收日志信息,生成日志前缀:[时间戳][日志级别][进程PID][源文件][行号],operator<<重载返回*this,支持链式调用,使用模板支持任意可流式输出的类型,每次<<都追加到_loginfo,日志消息在临时对象析构时输出。

<8> logger
cpp 复制代码
class logger
{
public:
    logger():_ptr(nullptr)
    {
        enableconsolelogstrategy();
    }
    void enableconsolelogstrategy()
    {
        _ptr=make_unique<consolelogstrategy>();
    }
    void enablefilelogstrategy()
    {
        _ptr=make_unique<filelogstrategy>();
    }​
    class logmessage
    {
    public:
        logmessage(const string& src,loglevel level,int num,logger& logger)
        :_pid(getpid())
        ,_src(src)
        ,_num(num)
        ,_curr_time(gettimestamp())
        ,_level(level)
        ,_logger(logger)
        {
            stringstream ss;
            ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["           <<_src<<"]"<<"["<<_num<<"]"<<"-";
            _loginfo=ss.str();
        }
        template<class K>
        logmessage& operator<<(const K& info)
        {
            stringstream ss;
            ss<<info;
            _loginfo+=ss.str();
            return *this;
        }
        ~logmessage()
        {
            if(_logger._ptr)
            {
                _logger._ptr->synclog(_loginfo);
            }
        }
    private:
        string _curr_time;
        loglevel _level;
        pid_t _pid;
        string _src;
        int _num;
        string _loginfo;
        logger& _logger;
    };
    logmessage operator()(loglevel lev,const string& name,int line)
    {
        return logmessage(name,lev,line,*this);
    }
private:
    unique_ptr<logstrategy> _ptr;
};

enableconsolelogstrategy(),logger构造函数默认启用控制台输出日志信息,_ptr为策略接口指针,支持运行时切换输出方式,logmessage为logger内部类,operator()返回一个logmessage对象,用于日志信息的输出。

<9> 全局实例与宏
cpp 复制代码
static logger log;
#define LOG(level) logmodule::log(level,__FILE__,__LINE__)
#define Enable_Console_Log_Strategy() logmodule::log.enableconsolelogstrategy()
#define Enable_File_Log_Strategy() logmodule::log.enablefilelogstrategy()

log为全局唯一的日志器实例,LOG(level)自动填充文件名和行号,Enable_Console_Log_Strategy()、Enable_File_Log_Strategy()为日志策略切换接口。

(4)inetaddr

inetaddr模块用于实现一个网络地址封装类,负责处理IPv4地址字符串与二进制格式转换、主机序与网络序转换、统一接口。

inetaddr.hpp

<1> 构造函数
cpp 复制代码
#ifndef _INETADDR_HPP_
#define _INETADDR_HPP_
#include"common.hpp"
using namespace std;
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(ipbuffer));
        _ip=ipbuffer;
    }
    inetaddr(const 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_port=htons(_port);
        _addr.sin_addr.s_addr=INADDR_ANY;
    }
    ~inetaddr()
    {}
private:
    struct sockaddr_in _addr;
    uint16_t _port;
    string _ip;
};
#endif

成员变量_addr为底层C结构体,数据存储格式为网络字节序,_port为主机字节序的端口号,_ip为点分十进制的IP字符串。构造函数可以从sockaddr_in开始构造,拷贝addr到_addr,_port=ntohs(_addr.sin_port),inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(ipbuffer)),分别从_addr.sin_port、_addr.sin_addr提取端口、IP。第二种是从IP字符串和端口进行构造,初始化_addr结构体,_addr.sin_family=AF_INET,inet_pton用于将点分十进制IP转化为二进制网络字节序,htons用于将主机字节序端口转化为网络字节序。最后一种是仅通过端口进行构造,适用于服务器绑定端口,监听所有网络接口,_addr.sin_addr.s_addr=INADDR_ANY,表示绑定到本机的所有IP地址。

<2> 访问器
cpp 复制代码
uint16_t port()
{
    return _port;
}
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 _ip==addr._ip && _port==addr._port;
}
string StringAddr()
{
    return _ip+":"+to_string(_port);
}

port用于获取端口号;ip用于获取IP字符;netaddr用于获取底层结构体引用;netaddrptr用于获取指向sockaddr的指针;netaddrlen用于获取结构体长度;operator==重载用于比较两个地址是否相同,即IP和端口号都相等;StringAddr用于在日志中输出IP、端口号信息。

(5)tcpserver

tcpserver模块封装了从创建监听套接字到处理客户端请求的完整流程,包括创建套接字、绑定、监听、接受连接、处理客户端请求。

<1> 初始化
cpp 复制代码
#ifndef _TCPSERVER_HPP_
#define _TCPSERVER_HPP_
#include"common.hpp"
#include"log.hpp"
#include"inetaddr.hpp"
#include<sys/wait.h>
#include<signal.h>
using namespace logmodule;
using namespace std;
const static int defaultsocketfd=-1;
const static int backlog=8;
using func_t =function<string(const string&,inetaddr&)>;
class tcpserver:public nocopy
{
public:
    tcpserver(uint16_t port,func_t func)
    :_port(port)
    ,_listensocketfd(defaultsocketfd)
    ,_isrunning(false)
    ,_func(func)
    {}
    void init()
    {
        _listensocketfd=socket(AF_INET,SOCK_STREAM,0);
        if(_listensocketfd<0)
        {
            LOG(loglevel::FATAL)<<"socket error";
            exit(SOCKET_ERR);
        }
        LOG(loglevel::INFO)<<"socket success"<<_listensocketfd;
        inetaddr local(_port);
        int n=bind(_listensocketfd,local.netaddrptr(),local.netaddrlen());
        if(n<0)
        {
            LOG(loglevel::FATAL)<<"套接字绑定失败";
            exit(BIND_ERR);
        }
        LOG(loglevel::INFO)<<"套接字绑定成功";
        n=listen(_listensocketfd,backlog);
        if(n<0)
        {
            LOG(loglevel::FATAL)<<"listen error";
            exit(LISTEN_ERR);
        }
        LOG(loglevel::INFO)<<"listen success"<<_listensocketfd;
    }
    ~tcpserver()
    {}
private:
    uint16_t _port;
    int _listensocketfd;
    bool _isrunning;
    func_t _func;
};
#endif

defaultsocketfd为套接字文件描述符的默认值,backlog为listen全连接队列最大长度,func_t为业务处理函数的类型。tcpserver继承nocopy类,不可被拷贝,第一个参数为客户端发送的原始数据,第二个参数为客户端的地址信息(IP+端口),返回值为将要回发给客户端的响应数据,这样使得服务器框架与业务逻辑解耦,用户可通过传入回调函数定制行为。_port为监听端口,_listensocketfd为监听套接字文件描述符,_isrunning为服务器运行状态标志,_func为业务处理回调函数。构造函数初始化成员变量,_listensocketfd=socket(AF_INET,SOCK_STREAM,0),init函数调用socket创建监听套接字,bind(_listensocketfd,local.netaddrptr(),local.netaddrlen()),通过bind绑定相应端口和地址,n=listen(_listensocketfd,backlog),调用listen开始监听,建立连接队列。

<2> service
cpp 复制代码
void service(int sockfd,inetaddr& peer)
{
    char buffer[1024];
    while(true)
    {
        ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            LOG(loglevel::DEBUG)<<peer.StringAddr()<<"#"<<buffer;
            string echo=_func(buffer,peer);
            write(sockfd,echo.c_str(),echo.size());
        }
        else if(n==0)
        {
            LOG(loglevel::DEBUG)<<peer.StringAddr()<<"退出了";
            close(sockfd);
            break;
        }
        else
        {
            LOG(loglevel::DEBUG)<<peer.StringAddr()<<"异常";
            close(sockfd);
            break;
        }
    }
}

service是处理客户端连接的核心函数,接收已建立的客户端套接字和对应的地址信息作为参数。内部通过一个无限循环来持续为客户端提供服务,ssize_t n=read(sockfd,buffer,sizeof(buffer)-1),调用read从套接字读取数据到固定大小的缓冲区,若n>0,则成功读取到数据,buffer[n]=0,添加空字符使其成为合法的C字符串,string echo=_func(buffer,peer),调用_func将原始数据与客户端地址传入,获取业务逻辑生成的响应字符串,write(sockfd,echo.c_str(),echo.size()),最后调用write将响应数据写回客户端。如果n==0,表明客户端主动关闭了连接,则会记录客户端退出的日志信息,若n<0,表示读取错误,同样会记录异常日志。

<3> run
cpp 复制代码
void run()
{
    _isrunning=true;
    while(_isrunning)
    {
        struct sockaddr_in addr;
        socklen_t len=sizeof(sockaddr_in);
        int sockfd=accept(_listensocketfd,CONV(addr),&len);
        if(sockfd<0)
        {
            LOG(loglevel::WARNING)<<"accept fail";
            continue;
        }
        inetaddr peer(addr);
        LOG(loglevel::INFO)<<"accept success,peer addr:"<<peer.StringAddr();
        service(sockfd,peer);
    }
    _isrunning=false;
}

run负责启动服务器并持续接收客户端连接,先将_isrunning标志设置为true,int sockfd=accept(_listensocketfd,CONV(addr),&len),通过while循环调用accept在监听套接字上阻塞等待,当有客户端连接请求到达时,accept会返回一个新的客户端套接字文件描述符,同时将客户端的地址信息填充到addr结构体中。若accept成功,inetaddr peer(addr),则使用addr构造一个inetaddr对象peer来封装客户端的IP地址和端口号,service(sockfd,peer),随后调用service方法并传入新获得的客户端套接字和peer对象,进入该客户端的读写服务循环。

(6)tcpserver.cc

cpp 复制代码
#include"tcpserver.hpp"
string defaulthandler(const string& word,inetaddr& addr)
{
    string s="hello,";
    s+=word;
    return s;
}
void usage(string proc)
{
    cerr<<"usage:"<<proc<<"port"<<endl;
}
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port=stoi(argv[1]);
    Enable_Console_Log_Strategy();
    unique_ptr<tcpserver> up=make_unique<tcpserver>(port,defaulthandler);
    up->init();
    up->run();
    return 0;
}

tcpserver.cc是TCP服务器的入口程序,defaulthandler函数接收一个字符串和客户端地址引用,在原始字符串前加上"hello,"前缀后返回。main主函数调用Enable_Console_Log_Strategy启用控制台日志输出,uint16_t port=stoi(argv[1]),将命令行第2个参数转化为uint16_t类型的端口号,然后创建一个unique_ptr智能指针指向tcpserver对象,构造函数中传入端口号和defaulthandler回调函数,随后依次调用up->init执行套接字的创建、绑定和监听,再调用up->run启动主循环接受并处理客户端连接。

(7)tcpclient.cc

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<string>
#include"common.hpp"
#include"inetaddr.hpp"
using namespace std;
void usage(string proc)
{
    cerr<<"usage:"<<proc<<"ip port"<<endl;
}
int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    string ip=argv[1];
    uint16_t port=stoi(argv[2]);
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        cerr<<"套接字创建失败"<<endl;
        exit(SOCKET_ERR);
    }
    inetaddr serveraddr(ip,port);
    int n=connect(sockfd,serveraddr.netaddrptr(),serveraddr.netaddrlen());
    if(n<0)
    {
        cerr<<"connect error"<<endl;
        exit(CONNECT_ERR);
    }
    while(true)
    {
        string line;
        cout<<"please enter:";
        getline(cin,line);
        write(sockfd,line.c_str(),line.size());
        char buffer[1024];
        ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"server echo:"<<buffer<<endl;
        }
    }
    close(sockfd);
    return 0;
}

tcpclient.cc是TCP客户端的入口程序,string ip=argv[1],uint16_t port=stoi(argv[2]),将命令行第二个参数作为IP字符串、第三个参数转换为uint16_t类型的端口号,int sockfd=socket(AF_INET,SOCK_STREAM,0),调用socket创建一个IPv4的TCP套接字,inetaddr serveraddr(ip,port),通过ip和端口构造一个inetaddr对象serveraddr,该对象内部完成地址的字节序转换和二进制格式转换。int n=connect(sockfd,serveraddr.netaddrptr(),serveraddr.netaddrlen()),调用connect函数发起与服务器的连接,连接成功后,通过while循环,getline(cin,line),使用getline从标准输入读取一行用户输入的字符串,write(sockfd,line.c_str(),line.size()),调用write将该字符串发送给服务器,ssize_t n=read(sockfd,buffer,sizeof(buffer)-1),调用read阻塞等待服务器的响应数据,循环退出后,程序调用close关闭套接字并返回。

(8)测试

cpp 复制代码
.PHONY:all
all:tcpclient tcpserver
tcpclient:tcpclient.cc
	g++ -o $@ $^ -std=c++17 
tcpserver:tcpserver.cc
	g++ -o $@ $^ -std=c++17 
.PHONY:clean
clean:
	rm -rf tcpclient tcpserver

通过Makefile即可实现自动化编译TCP客户端和服务端程序,g++ -o @ ^ -std=c++17 ,编译选项启用C++17标准,将生成tcpclient、tcpserver两个可执行文件。编译通过后,先启动服务器,指定端口号8889,./tcpserver 8889,在另一个终端启动客户端,./tcpclient 127.0.0.1 8889,127.0.0.1为本地环回地址,客户端和服务器运行结果如下所示:

服务器:

可以看到,服务器依次打印日志:socket success、套接字绑定成功、listen success,然后进入阻塞状态等待客户端连接。

客户端:

在另一个终端启动客户端,可以看到客户端连接成功后,终端显示please enter,等待用户输入,用户在客户端输入内容后,客户端将内容发送给服务器,服务器收到数据后打印调试日志,显示客户端的IP地址和端口以及接收到的内容,调用业务处理函数加上hello发回客户端,客户端收到服务器响应后在终端显示。

结语

从socket API的简单调用到Echo Server回显服务器的实现,TCP通信的每一个环节都体现了可靠传输的设计哲学,从这个简单的Echo Server出发,服务端通过socket、bind、listen、accept搭建监听框架,客户端通过socket和connect发起连接,双方通过read、write在已建立的通道上交换数据,最后通过close释放资源。Echo Server的实现,背后依赖的是TCP协议栈提供的可靠传输、顺序交付、流量控制与拥塞控制等一整套复杂机制,理解这个服务器背后从API到内核实现的完整路径,是迈向复杂网络程序开发的重要一步。

相关推荐
_深海凉_2 小时前
LeetCode热题100-有效的括号
linux·算法·leetcode
你的保护色2 小时前
ensp 路由器启动失败 解决方案
网络
PinTrust SSL证书2 小时前
IP地址访问网站,怎么去除不安全提示?
网络协议·tcp/ip·安全·网络安全·https·ssl
2501_913061343 小时前
网络原理知识
java·网络
零号全栈寒江独钓4 小时前
基于c/c++实现linux/windows跨平台获取ntp网络时间戳
linux·c语言·c++·windows
左手厨刀右手茼蒿4 小时前
Linux 内核中的进程管理:从创建到终止
linux·嵌入式·系统内核
geinvse_seg4 小时前
中小团队如何低成本搭建项目管理系统?基于 Ubuntu 的 Dootask 私有化部署实战
linux·运维·ubuntu
CSCN新手听安4 小时前
【linux】高级IO,以ET模式运行的epoll版本的TCP服务器实现reactor反应堆
linux·运维·服务器·c++·高级io·epoll·reactor反应堆
丶伯爵式4 小时前
Ubuntu 24.04 更换国内软件源指南 | 2026年3月26日
linux·运维·ubuntu·国内源·升级