目录
[2、Echo Server](#2、Echo Server)
[<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> 全局实例与宏)
[<1> 构造函数](#<1> 构造函数)
[<2> 访问器](#<2> 访问器)
[<1> 初始化](#<1> 初始化)
[<2> service](#<2> service)
[<3> run](#<3> run)
前言
在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到内核实现的完整路径,是迈向复杂网络程序开发的重要一步。