上一篇:socket套接字-UDP(下)https://blog.csdn.net/Small_entreprene/article/details/147569071?fromshare=blogdetail&sharetype=blogdetail&sharerId=147569071&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link在套接字编程中,UDP是不面向连接,面向数据报的一种通信方式,UDP套接字创建好,直接发送消息就可以通信,而TCP是面向连接的,注定了要比UDP要复杂一点点,但是两者代码的主体是很像的。
一样的,我们通过TCP编程来实现一个最简单的功能性验证的服务---Echo Server
学前补充
telnet
我们这一篇使用这个命令是为了进行内网环境调试(如测试服务端口)(监听---创建连接)(相当于客户端与远端正在监听的服务器建立连接),方便我们在实现TcpClient.cc之前的测试!!!下面我们来介绍一下这个命令的用法:
Telnet(Telecommunication Network)是一种基于 TCP/IP 协议 的远程终端连接协议,允许用户通过网络连接到远程主机并执行命令行操作。
Telnet 本质上是一个客户端-服务器模型,客户端通过 TCP 协议与远端正在监听的服务器建立连接,并通过该连接进行交互。
-
默认端口:23(可自定义)。
-
特点:明文传输(不加密),适用于局域网或受信任环境。
-
历史地位:曾是远程管理的主流工具,现因安全性问题逐渐被 SSH 取代。
在我的Linux的Ubuntu系统下,我们要先安装telnet:
sudo apt update
sudo apt install telnet # 安装客户端
sudo apt install telnetd # 安装服务端(不推荐开启)
基本使用方法:
telnet [选项] [目标主机] [端口]
常用选项:
-
-l <用户>
:指定登录用户名(需服务端支持)。 -
-4
/-6
:强制使用 IPv4 或 IPv6。
我们据此就可以进行尝试与远端服务器进行建立连接:
telnet 192.168.1.100 # 默认端口23
telnet 127.0.0.1 8080
telnet example.com 80 # 测试 HTTP 服务端口
telnet example.com 22 # 测试 SSH 服务端口
下面,我们通过 Telnet 客户端尝试与 百度服务器(www.baidu.com)的 80 端口 建立 TCP 连接:
对于TCP的相关接口,会在下面实现代码的过程中一一解释!
单进程版本的Echo Server
在网络编程的江湖中,TCP 和 UDP 是两大门派,各有所长,各怀绝技。今天,咱们就以实现一个单进程 TCP 服务端为引子,把 TCP 的武功秘籍拆解得清清楚楚,同时一路对比 UDP 的招式,让你看得透彻,学得扎实。
一、缘起:为什么要实现 TCP 服务端?服务器有什么要求吗?
先说个眼前的场景:你要做一个远程控制小车的服务端,客户端是手机 APP。这玩意儿得实时、可靠地传输指令和状态,丢包、乱序什么的都不行。UDP?算了,它的 “发完就不管” 性格在这儿容易出岔子。还是 TCP 靠谱,有握手、有挥手、有确认,一步步来,稳得很。
好,现在咱就从零开始,实现一个单进程的 TCP 服务端,服务端得能接收客户端的连接,收数据,再把数据原封不动地扔回去(也就是 “回显” 功能)。这功能虽简单,但五脏俱全,正好是学习 TCP 编程的绝佳练手项目。
其实服务器都是禁止拷贝的!!!
服务器类(如 TcpServer
)禁止拷贝主要有以下原因:服务器类通常拥有大量独一无二的资源,像文件描述符、内存缓冲区等,若允许拷贝,会导致多个对象同时拥有同一份资源,引发资源管理混乱,如关闭套接字时出现错误。同时,服务器类代表具有特定功能和状态的实体,逻辑上应独一无二,允许拷贝会导致多个实体处理同一任务,不符合实际需求。此外,服务器对象内部有复杂的状态,允许拷贝难以保证状态一致性,容易出现竞态条件等并发问题。综上,禁止拷贝能确保服务器类对象正确、安全地管理资源,保证其逻辑完整性和行为可预测性,符合设计初衷和实际使用需求。
服务端类通过继承 NoCopy
类并定义删除拷贝构造函数和拷贝赋值运算符的方法来禁止拷贝是合理的!
下面,我先提前做一些准备工作:
Common.hpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定义一个枚举类型ExitCode,用于表示程序的不同退出状态码
enum ExitCode
{
OK = 0, // 程序正常退出
USAGE_ERR, // 用户使用错误,如参数错误等
SOCKET_ERR, // 创建套接字失败
BIND_ERR, // 套接字绑定地址失败
LISTEN_ERR, // 套接字监听失败
CONNECT_ERR, // 套接字连接失败
FORK_ERR // fork创建子进程失败
};
// 定义一个类NoCopy,用于禁止类的拷贝行为
class NoCopy
{
public:
// 构造函数
NoCopy() {}
// 析构函数
~NoCopy() {}
// 删除拷贝构造函数,阻止对象的拷贝构造
NoCopy(const NoCopy &) = delete;
// 删除拷贝赋值运算符,阻止对象的拷贝赋值
const NoCopy &operator=(const NoCopy &) = delete;
};
// 定义宏CONV,用于将sockaddr_in类型的地址转换为sockaddr类型的地址
#define CONV(addr) ((struct sockaddr *)&addr)
在 C++ 中,通过将拷贝构造函数和拷贝赋值运算符设置为 delete
,并在类 NoCopy
中定义这些被删除的特殊成员函数,子类 TcpServer
继承 NoCopy
后,就能禁止拷贝。因为继承会使得子类继承基类的这些被删除的特殊成员函数,当试图对 TcpServer
对象进行拷贝操作时,编译器会检查到拷贝构造函数和拷贝赋值运算符被删除,从而报错,这样就实现了禁止拷贝的目的。
// 服务器往往是禁止拷贝的
using namespace LogModule;
const static int defaultsockfd = -1;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port)
: _port(port),
_listensockfd(defaultsockfd),
_isrunning(false),
{ }
void Init()
{
//......
}
void Service(int sockfd, InetAddr &peer)
{
//......
}
void Run()
{
//......
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listensockfd; // 监听socket
bool _isrunning;
};
InetAddr.hpp
#pragma once
#include "Common.hpp"
// 定义一个 InetAddr 类,用于在 IPv4 网络地址和主机地址之间进行转换
class InetAddr
{
public:
// 默认构造函数
InetAddr() {}
// 通过已存在的 sockaddr_in 类型的地址初始化
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
// 将网络字节序的端口号转换为主机字节序
_port = ntohs(_addr.sin_port);
// 将网络字节序的 IPv4 地址转换为字符串形式(点分十进制)
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
// 通过指定的 IP 地址(字符串形式)和端口号初始化
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
// 初始化 sockaddr_in 结构体为全零
memset(&_addr, 0, sizeof(_addr));
// 设置地址族为 IPv4
_addr.sin_family = AF_INET;
// 将 IPv4 地址从字符串形式转换为网络字节序
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
// 将主机字节序的端口号转换为网络字节序
_addr.sin_port = htons(_port);
}
// 通过指定的端口号初始化(自动绑定到任意 IP 地址)
InetAddr(uint16_t port) : _port(port), _ip()
{
// 初始化 sockaddr_in 结构体为全零
memset(&_addr, 0, sizeof(_addr));
// 设置地址族为 IPv4
_addr.sin_family = AF_INET;
// 设置 IP 地址为 INADDR_ANY,表示任意 IP 地址
_addr.sin_addr.s_addr = INADDR_ANY;
// 将主机字节序的端口号转换为网络字节序
_addr.sin_port = htons(_port);
}
// 获取端口号
uint16_t Port() { return _port; }
// 获取 IP 地址(字符串形式)
std::string Ip() { return _ip; }
// 获取 sockaddr_in 类型的网络地址
const struct sockaddr_in &NetAddr() { return _addr; }
// 获取 sockaddr 类型的网络地址指针
const struct sockaddr *NetAddrPtr()
{
return CONV(_addr); // 使用宏将 sockaddr_in 转换为 sockaddr 指针
}
// 获取网络地址的长度
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
// 重载 == 操作符,用于比较两个 InetAddr 对象是否相等
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
// 获取地址的字符串形式(IP:端口)
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
// 析构函数
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 存储网络地址的 sockaddr_in 结构体
std::string _ip; // 存储 IP 地址的字符串形式
uint16_t _port; // 存储端口号(主机字节序)
};
其他相关代码之前就应景写过了!
启动服务器:
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Run();
TCP服务端工作流程图解
-
socket():创建通信端点
-
bind():绑定地址信息
-
listen():设置监听队列
-
accept():接受新连接
-
read()/write():数据读写(面向字节流的)
二、单进程 TCP 服务端的实现之路
TCP服务端工作流程图解:单进程
(一)第一步:创建套接字(socket)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 1. 创建套接字文件
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3
这行代码就是咱们TCP服务端开山凿石的第一锤。socket()
函数创建一个套接字,这就好比是打通了一条能和外界通信的暗道。它的三个参数里,AF_INET
指定了用 IPv4 地址族(简单说就是咱们常用的 IPv4 地址那一套);SOCK_STREAM
就是 TCP 的标志,告诉系统这是一条要可靠传输的路;最后一个 0 是默认协议,一般不用管。
这和UDP的创建套接字有啥不一样?UDP也是用 socket()
,但它用的是 SOCK_DGRAM
(数据报的意思),一上来就定了UDP是传数据报的,不像TCP要搞复杂的连接管理。
要是创建失败,socket()
会返回 -1。所以后面得加个判断,失败了就报个错,别让程序默默死掉。
(二)第二步:绑定套接字(bind)
// 2. bind众所周知的端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3
这儿的 bind()
就是给刚才挖好的通信暗道(套接字)找个具体的位置,让它能被人找着。local.NetAddrPtr()
这个结构体就是地址信息,在InetAddr类当中封装了相关信息:INADDR_ANY
表示监听这台机器上所有网卡的地址,不管客户端从哪个网口来,都能被接到。htons(...)
是把端口号转换成网络字节序,网络上数据传输都按这规矩来。
UDP 也有绑定这一步,而且用法几乎一样。为啥 TCP 和 UDP 都得绑定?因为不管哪种通信,系统都得知道你这程序在哪儿 “摆摊”,好把数据送到这儿来。
要是绑定失败,返回值会小于 0,后面得处理这个错误,不然服务端就 “隐形” 了,客户端找不到。
(三)第三步:监听连接(listen)
listen()
是 Linux C++ 网络编程中的关键系统调用,用于将一个已绑定的套接字设置为被动监听模式,准备接受客户端的连接请求。它是 TCP 服务器编程中必不可少的一步。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
sockfd
: 已绑定(bind)的套接字描述符 -
backlog
: 等待连接队列的最大长度,决定了内核为此套接字排队的最大连接数 -
成功时返回 0;失败时返回 -1,并设置
errno
const static int backlog = 8;
// 3. 设置socket状态为listen
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3
这一步是 TCP 的独家绝技。listen()
让套接字进入 “等待连接” 的状态,后面的 backlog 是说最多同时排 8 个连接在队里等着。要是有第 9 个来,就直接拒了。
UDP 没这一步,因为 UDP 是无连接的,客户端的数据报过来就直接收,没有 “先排个队,等我一个个理” 这个过程。
要是监听失败,返回值又会小于 0,必须得加个错误处理。不然服务端就卡在这儿了,连 “我挂了” 的消息都发不出来。
目前,Init相关接口就已经完成了:
void Init()
{
// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法
// 1. 创建套接字文件
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3
// 2. bind众所周知的端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3
// 3. 设置socket状态为listen
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3
}
我们可以使用相关网络命令:(网络命令会整合在一篇文章当中)来查看一下我们当前的初始化服务器的状态:
(四)第四步:接受连接(accept)
通过 telnet
连接到服务器,但如果没有 accept
,服务器无法为客户端提供服务。accept
是从监听套接字(listensockfd
)中提取连接请求并创建独立通信套接字(sockfd
)的关键步骤,它使得服务器能够真正与客户端建立连接并处理请求。
accept()
是 Linux C++ 网络编程中的一个重要系统调用,用于在服务器端接受来自客户端的连接请求。它是基于 TCP 协议的 socket 编程中的关键函数之一。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
sockfd
: 监听套接字描述符,通常由socket()
创建并通过bind()
和listen()
初始化的套接字 -
addr
: 指向sockaddr
结构的指针,用于存储客户端的地址信息 -
addrlen
: 指向socklen_t
的指针,指定addr
缓冲区的大小,调用后会被设置为实际地址的长度 -
成功时返回一个新的套接字描述符,用于与客户端通信;失败时返回 -1,并设置
errno
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
accept()
又是 TCP 的看家本领。服务端在这儿等着,有客户端来连了,就从刚才那个队列里把连接取出来。peer
会记录下客户端的地址信息(IP 和端口)。
UDP 呢?它没这招。UDP 收数据直接用 recvfrom()
,一边收数据一边还能知道是谁发的(也能获取客户端地址),但没有 TCP 这种先连上、再通信的流程。
要是 accept()
失败了,返回值是 -1。不过大多数时候,它是会阻塞在这儿等着连接来的。要是这儿出错了,后面得处理好,别让服务端直接垮掉。
要注意的是:
在 TCP 套接字编程里,先调用 listen 函数处于监听状态后,才会有建立连接的动作。后续的 accept 接口,成功返回的文件描述符,与最初创建 TCP 套接字时形成的文件描述符有区别。 accept 获取的链接是从内核中直接获取的,建立连接的过程与 accept 并无关联。就好比好再来鱼庄的故事:张三(listensockfd)在马路上拉客,拉到客人后,叫李四(sockfd)来服务,张三继续拉客。马路相当于操作系统,客人就是从内核来的连接请求,好再来鱼庄就是服务器, listensockfd 只负责拉客(连接), accept 的返回值(sockfd)是给客户提供服务的。
当执行 listen 操作后,才会真正开始建立连接,此前一直处于监听状态。而 accept 接口成功返回的文件描述符和最初创建 TCP 套接字的文件描述符有所不同。 accept 获取的链接源自内核。建立连接的操作与 accept 本身无关。以好再来鱼庄为例,张三(_listensockfd)在马路上拉客,成功后不是自己服务客人,而是让李四(_sockfd)来服务。张三拉不到客人不会停下,而是继续拉客。(continue)马路就像操作系统,客人是从内核来的,好再来鱼庄是服务器。其中, listensockfd 专注于吸引连接, accept 返回的文件描述符(_sockfd)用于给客户提供服务。
在套接字通信中,只有在执行 listen 命令之后,才会触发建立连接的过程,此前系统处于监听状态。而 accept 函数成功执行后返回的文件描述符,与最初创建 TCP 套接字时得到的那个文件描述符并不相同。 accept 获取的链接实际上是从内核空间分配并提供的。建立连接的整个流程, accept 并不直接参与。可以类比好再来鱼庄的场景:张三(listensockfd)在马路上负责拉客,当拉到客人后,他不会亲自去服务,而是把客人交给李四(sockfd)等服务员来接待,然后自己继续在马路上拉客。这里的马路可以看作操作系统,客人就如同从内核而来的连接请求,好再来鱼庄就是服务器本身。在这个过程中, listensockfd 的任务就是吸引连接,而 accept 返回的文件描述符(sockfd)才是用来给客户提供具体服务的。
(五)第五步:收发数据(read 和 write)
void Service(int sockfd, InetAddr &peer)
{
char buffer[1024];
while (true)
{
// 1. 先读取数据
// a. n>0: 读取成功
// b. n<0: 读取失败
// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipe
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1
LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;
// 2. 写回数据
std::string echo_string = "echo# ";
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;
}
}
}
read()
从连接里收数据,write()
把数据送回去。这两个函数保证了数据按顺序、完整地传输。TCP 在底层做了很多工作,比如丢包了就重传,乱序了就排好序。
UDP 收发数据用的是 recvfrom()
和 sendto()
,这两个函数还得多带俩参数(客户端地址和地址长度),因为 UDP 没连着呢,每次发数据都得指明目的地。
要是读写失败,返回值是 -1,但一般只要连接正常,这两个操作就算不成功也会部分成功(比如网络不好,写了一半),所以后面也得处理错误,不然服务端可能在那儿傻等,啥也干不了。
因为TCP是面向字节流的,所以使用read/write这样的流式接口,后面会提到针对TCP的读写,有对应的读写接口!
(六)第六步:关闭连接
close(_sockfd);
服务端处理完一个客户端,就把这次连接专用的文件描述符(_sockfd
)关了。不过监听用的 _listensockfd
还开着呢,还能接新的连接。
UDP 不需要这一步,因为它本来就没连上,数据报发完就完了,没有 “关闭连接” 的概念。但如果不用了,UDP 的套接字也得用 close()
关掉。
自此,针对单进程模式,我们的Run接口就实现好了:
void Run()
{
_isrunning = true;
while (_isrunning)
{
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
// version0 -- test version --- 单进程程序 --- 不会存在的!
Service(sockfd, addr);
}
_isrunning = false;
}
三、单进程 TCP 服务端的局限性:为什么它不够用?
写完上面的代码,你要是运行起来试试,会发现个问题:只能同时和一个客户端通信。要是有第二个客户端连上来,服务端还在和第一个聊天呢,第二个就得等着。要是第一个客户端一直占着线,服务端就一直不搭理新的连接。
为啥?因为这是个单进程程序。它处理一个连接的时候,其他的都得排队。这就好比是个小摊贩,一次只能服务一个顾客,别的顾客都得在那干等着。
那怎么办?这就得学更高级的招式了:多进程、多线程,甚至线程池。这些招式能让你的服务端同时服务多个客户端,效率高得多。
再用telnet,自己的客户端还写不写!TcpClient
在 TCP 服务端实现之后,我们自然要实现一个与之配套的 TCP 客户端。在实现的过程中,我们会详细介绍每个接口的作用,并与 UDP 客户端实现进行对比,让你清晰地看到 TCP 和 UDP 在客户端编程中的差异。
一、TCP 客户端的实现之路
(一)第一步:创建套接字(socket)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
这一步与 TCP 服务端的创建套接字类似。socket()
函数创建一个套接字,其中 SOCK_STREAM
表示这是一个 TCP 套接字。这一步是客户端和服务器通信的起点。
TCP 客户端和服务端都使用 socket()
创建套接字,是因为 socket()
提供了一个统一的接口,用于初始化网络通信所需的资源。虽然它们在创建套接字后的操作有所不同,但 socket()
为它们提供了一个通用的起点。
对比 UDP 客户端: UDP 客户端也使用 socket()
创建套接字,但类型为 SOCK_DGRAM
,表示这是一个无连接的数据报套接字。
TCP 服务端需要绑定地址和端口,以便客户端连接。TCP 客户端通常不需要显式绑定,由系统自动分配端口。
TCP 服务端需要调用 listen()
进入监听状态,等待客户端连接。TCP 客户端不需要监听。
TCP 服务端使用 accept()
接受客户端连接,获取新的套接字用于通信。TCP 客户端不需要接受连接。
(二)第二步:连接到服务器(connect)
// 2. bind吗??需要。显式的bind?不需要!随机方式选择端口号
// 2. 我应该做什么呢?listen?accept?都不需要!!
// 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);
}
connect()
是 TCP 客户端的关键步骤,用于建立与服务器的连接。servaddr
包含服务器的 IP 地址和端口号。connect()
发起连接请求,若服务器接受,则连接建立。
对比 UDP 客户端: UDP 是无连接的,UDP 客户端不需要 connect()
,可直接发送数据报。UDP 使用 sendto()
和 recvfrom()
发送和接收数据,无需事先建立连接。
(三)第三步:发送和接收数据(write 和 read)
// 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;
}
}
write()
和 read()
用于 TCP 客户端的发送和接收数据。write()
将数据发送到服务器,read()
从服务器接收数据。TCP 保证数据的顺序和完整性。
对比 UDP 客户端: UDP 客户端使用 sendto()
和 recvfrom()
发送和接收数据。sendto()
需要指定目标地址,recvfrom()
可获取发送方地址。UDP 不保证数据顺序和完整性。
(四)第四步:关闭连接(close)
close(sockfd);
连接完成后,客户端关闭套接字以释放资源。这一步在 TCP 和 UDP 客户端中相同。
二、完整的 TCP 客户端代码
#include <iostream> // 引入输入输出流库
#include "Common.hpp" // 引入自定义的公共头文件,包含错误码等定义
#include "InetAddr.hpp" // 引入处理网络地址的头文件
// 定义使用函数打印使用方式
void Usage(std::string proc) {
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// 主函数入口
int main(int argc, char *argv[]) {
// 检查命令行参数数量是否正确(程序名 + 2个参数)
if (argc != 3) {
Usage(argv[0]); // 打印使用方式
exit(USAGE_ERR); // 退出程序,并返回错误码
}
std::string serverip = argv[1]; // 获取服务器IP地址
uint16_t serverport = std::stoi(argv[2]); // 获取服务器端口号
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
if (sockfd < 0) { // 检查socket创建是否成功
std::cerr << "socket error" << std::endl; // 打印错误信息
exit(SOCKET_ERR); // 退出程序,并返回错误码
}
// 2. 客户端不需要bind操作,因为connect会自动分配端口号
// 3. 直接向目标服务器发起建立连接的请求
InetAddr serveraddr(serverip, serverport); // 创建服务器地址对象
int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen()); // 连接服务器
if (n < 0) { // 检查连接是否成功
std::cerr << "connect error" << std::endl; // 打印错误信息
exit(CONNECT_ERR); // 退出程序,并返回错误码
}
// 4. 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); // 关闭socket
return 0; // 正常退出程序
}
多进程版本的Echo Server
TCP服务端工作流程图解:多进程
在我们fork创建子进程的时候,子进程退出需要被父进程wait回收,但是如果单纯这个实现的话,就和上面的单进程模式没有什么区别了,我们之前学过,我们可以设置信号来实现父进程忽略其子进程来实现非阻塞,使用signal,这样,程序就可以并行了,主线程主要负责,子线程实现具体的服务操作。
设置信号处理函数忽略 SIGCHLD
信号
signal(SIGCHLD, SIG_IGN);
父进程设置信号处理函数忽略 SIGCHLD
信号,这样当子进程退出时,操作系统会自动回收子进程资源,避免父进程阻塞在 waitpid()
上。
我们还有一个更加巧妙的方式,利用孤儿进程是被1号进程托管回收的原理,我们可以让其子进程创建子进程,即对应父/主进程的孙子进程,这样,发进程waitpid子进程,子进程就创建孙子进程,让孙子进程来负责主要服务操作,子进程被回收,孙子进程就变成了孤儿进程,会自动被1号进程管理,这样,我们就巧妙的从串行,实现了并发效果了,提高效率还解决了上面单进程带来的占线问题!
-
主线程 :负责初始化服务端的网络资源,接受客户端的连接请求,并为每个连接创建一个子进程。主线程通过
waitpid()
回收子进程,确保子进程不会成为僵尸进程。 -
子进程 :为每个客户端连接创建一个孙子进程,然后立即退出。子进程的作用是快速创建孙子进程并退出,避免阻塞主线程。
-
孙子进程 :负责实际的客户端通信,处理客户端的请求并返回响应。孙子进程在处理完客户端请求后退出,被 1 号进程接管回收。
在实现多进程模式的改造的时候,我们应该注意:
在多进程版本的 TCP 服务端中,正确关闭文件描述符是非常重要的。每个进程(主线程、子进程、孙子进程)都需要关闭它们不再需要的文件描述符。
正确关闭文件描述符是多进程 TCP 服务端中的一个重要环节。每个进程(主线程、子进程、孙子进程)都需要关闭它们不再需要的文件描述符,以避免资源泄漏和文件描述符耗尽的问题。具体来说:
-
主线程 :关闭客户端连接套接字(_sock
fd
),保持监听套接字(_listensockfd
)打开。 -
子进程 :关闭监听套接字(_
listensockfd
)和客户端连接套接字(_sockfd)。 -
孙子进程 :关闭客户端连接套接字(_sock
fd
)。
不关闭的后果:
-
资源泄漏 :如果孙子进程不关闭
connfd
,系统会一直保留这个文件描述符,占用系统资源。 -
文件描述符耗尽 :随着孙子进程的增加,系统中的文件描述符数量会不断增加,最终可能导致系统资源耗尽,服务端无法接受新的连接。
通过正确管理文件描述符,可以确保服务端高效、稳定地运行,避免资源耗尽和性能问题。
多进程的改造代码:
void Run()
{
_isrunning = true;
while (_isrunning)
{
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
// version0 -- test version --- 单进程程序 --- 不会存在的!
// Service(sockfd, addr);
// version1 --- 多进程版本
pid_t id = fork(); // 父进程
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if (id == 0)
{
// 子进程,子进程除了看到sockfd,能看到listensockfd吗??
// 我们不想让子进程访问listensock!
close(_listensockfd);
if (fork() > 0) // 再次fork,子进程退出
exit(OK);
Service(sockfd, addr); // 孙子进程,孤儿进程,1, 系统回收我
exit(OK);
}
else
{
// 父进程
close(sockfd);
// 父进程是不是要等待子进程啊,要不然僵尸了??
pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
(void)rid;
}
}
_isrunning = false;
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listensockfd; // 监听socket
bool _isrunning;
//func_t _func; // 设置回调处理
};
多线程版本的Echo Server
TCP服务端工作流程图解:多线程
在多线程模式下,所有线程共享同一个进程的资源,包括文件描述符、内存空间等。这意味着每个线程都可以访问和操作同一个进程中的文件描述符。
线程间共享监听套接字(_listensockfd
)共享连接套接字(_sockfd)吗?
- 是的,线程间共享监听套接字。主线程创建的监听套接字(_
listensockfd
)是全局的,所有线程都可以访问它。但是,通常只有主线程会调用accept()
来接受新的连接请求,生成新的连接套接字(_sockfd
)。 - 不太是的,连接套接字(_sock
fd
)也会被线程共享。但是,通常每个连接套接字(_sockfd
)会被传递给一个单独的线程来处理,其他线程不会直接操作这个套接字。本质也就是说是不共享的!
主线程(父线程)
-
创建监听套接字,绑定地址和端口,进入监听状态。
-
接受客户端的连接请求,为每个连接创建一个线程。
-
分离线程,避免线程结束后需要手动回收。
-
关闭监听套接字,释放资源。
主线程的主要职责是创建监听套接字(_listensockfd
),绑定地址和端口,进入监听状态,并接受新的连接请求。每当有新的客户端连接时,主线程会创建一个新的线程来处理这个连接,并将连接套接字(_sockfd
)传递给这个线程。
关闭客户端连接套接字(connfd
) :主线程不需要客户端连接套接字,因此在创建线程后立即关闭它
close(_sockfd);
原因 :避免资源泄漏,确保主线程不会占用不必要的文件描述符。 不关闭的后果 :资源泄漏,文件描述符耗尽,服务端无法接受新的连接。
线程(子线程)
-
处理客户端的请求,读取客户端发送的数据并回显。
-
关闭连接套接字,释放资源。
每个线程的主要职责是处理一个客户端的连接。线程从主线程接收连接套接字(_sockfd
),并使用这个套接字与客户端进行通信。处理完成后,线程关闭连接套接字并退出。
关闭客户端连接套接字(connfd
) :线程在处理完客户端请求后关闭连接套接字。
close(_sockfd);
原因 :避免资源泄漏,确保线程不会占用不必要的文件描述符。 不关闭的后果 :资源泄漏,文件描述符耗尽,服务端无法接受新的连接。
多线程的改造代码:
using task_t = std::function<void()>;
TcpServer(uint16_t port, func_t func) : _port(port),
_listensockfd(defaultsockfd),
_isrunning(false),
_func(func)
{ }
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)
{
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
// version2: 多线程版本
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
_isrunning = false;
}
线程池版本的Echo Server
TCP服务端工作流程图解:线程池
线程池的主要职责是管理一组预先创建的线程,这些线程在空闲时等待任务,一旦有任务到来,就立即处理。线程池可以有效减少线程创建和销毁的开销,提高系统的并发处理能力。(线程池中的线程已经准备好,可以立即处理任务,减少了任务的等待时间,提高了系统的响应速度。)
在多线程模式下,TCP 服务端的实现主要依赖于主线程和线程池中的线程。主线程负责创建监听套接字,接受客户端的连接请求,并将连接套接字传递给线程池。线程池中的线程负责处理客户端的请求,读取客户端发送的数据并回显。每个线程在处理完客户端请求后关闭连接套接字,释放资源。
通过这种设计,可以有效利用线程池的并发能力,提高服务端的处理效率。同时,通过合理管理文件描述符和线程间资源共享,可以避免资源泄漏和同步问题。
线程池的改造代码:
void Run()
{
_isrunning = true;
while (_isrunning)
{
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
// version2: 多线程版本
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
// version3:线程池版本,线程池一般比较适合处理短服务
// 将新链接和客户端构建一个新的任务,push线程池中
ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){
this->Service(sockfd, addr);
});
}
_isrunning = false;
}
线程池版本的翻译Server
在设计一个线程池版本的翻译服务器时,我们可以利用 C++11 中的 std::function
来实现一个灵活的服务处理函数。这种设计允许我们轻松地将不同的服务逻辑(如翻译服务)插入到服务器中,从而实现分层解耦合。
1. 定义服务函数类型
首先,我们定义一个服务函数类型 func_t
,它是一个函数对象,接受一个字符串和 InetAddr
对象,返回一个字符串。
#include <functional>
#include <string>
using func_t = std::function<std::string(const std::string &, InetAddr &)>;
2. 实现一个翻译服务
接下来,我们实现一个具体的翻译服务。这个服务将接收一个字符串,将其翻译并返回翻译结果。
// 1. 翻译模块
Dict d;
d.LoadDict();
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&d](const std::string &word, InetAddr &addr)
{ return d.Translate(word, addr); });
多线程远程命令执行
在现代网络服务中,多线程模型因其能够处理大量并发请求而变得尤为重要。本文将介绍如何实现一个多线程的 TCP 服务端,并展示如何通过 std::function
来执行远程命令。
在多线程的服务端中,我们可以实现一个命令执行功能,允许客户端发送命令,服务端执行这些命令并返回结果。
1. 定义命令执行函数
我们定义一个 Command
类,它包含一个 Execute
方法,该方法接收一个命令字符串,执行该命令,并返回结果。
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include <unistd.h>
#include "Command.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogModule; // 使用日志模块的命名空间
class Command {
public:
// 构造函数,初始化白名单命令集
Command() {
// 将允许执行的安全命令添加到白名单集合中
_WhiteListCommands.insert("ls");
_WhiteListCommands.insert("pwd");
_WhiteListCommands.insert("ls -l");
_WhiteListCommands.insert("touch haha.txt");
_WhiteListCommands.insert("who");
_WhiteListCommands.insert("whoami");
}
// 检查命令是否在白名单中
bool IsSafeCommand(const std::string &cmd) {
auto iter = _WhiteListCommands.find(cmd);
return iter != _WhiteList.end(); // 如果迭代器未找到命令,则命令不在白名单中
}
// 执行命令并返回结果
std::string Execute(const std::string &cmd, InetAddr &addr) {
// 1. 检查命令是否在白名单中
if (!IsSafeCommand(cmd)) {
return std::string("unsafe"); // 如果命令不在白名单中,返回"unsafe"
}
std::string who = addr.StringAddr(); // 获取执行命令的客户端地址
// 2. 执行命令
FILE *fp = popen(cmd.c_str(), "r"); // 以只读方式打开命令对应的文件
if (fp == nullptr) {
return std::string("你要执行的命令不存在: ") + cmd; // 如果命令文件不存在,返回错误信息
}
std::string res; // 用于存储命令执行结果
char line[1024]; // 缓冲区,用于存储逐行读取的命令输出
while (fgets(line, sizeof(line), fp)) { // 逐行读取命令输出
res += line; // 将读取到的内容添加到结果字符串中
}
pclose(fp); // 关闭文件
std::string result = who + " execute done, result is: \n" + res; // 构造最终返回的结果字符串
LOG(LogLevel::DEBUG) << result; // 记录日志
return result; // 返回执行结果
}
// 析构函数
~Command() {}
private:
// 白名单命令集合
std::set<std::string> _WhiteListCommands; // 存储允许执行的命令集合
};
2. 线程函数处理客户端请求
每个线程将处理一个客户端请求。线程将读取客户端发送的命令,使用 Command
类执行该命令,并将结果发送回客户端。
// 1. 命令的执行模块
Command cmd;
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,
std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));
// 2. 启动服务器
tsvr->Init();
// 3. 运行服务器
tsvr->Run();
通过实现多线程的 TCP 服务端,我们能够有效地处理大量并发的客户端请求。利用 std::function
,我们可以灵活地扩展服务端的功能,例如添加新的命令执行逻辑。这种设计不仅提高了服务端的并发处理能力,还增强了其可扩展性和可维护性。
使用场景
(一)短服务
定义 :短服务是指客户端与服务器之间的交互通常是一次性的,请求和响应完成后,连接即被关闭。这种服务的特点是请求频率低、每次请求的数据量小。
(二)长服务
定义 :长服务是指客户端与服务器之间建立连接后,会保持连接状态较长时间,可以进行多次数据交换。这种服务的特点是请求频率高、实时性要求高。
二、多进程、多线程和线程池模式的适用性
(一)多进程模式
适用场景 :多进程模式适用于短服务。
原因 :
-
资源开销 :每次客户端连接都会创建一个新的进程,资源开销较大。对于短服务,这种开销是可以接受的,因为连接的生命周期短,不会占用太多资源。
-
隔离性 :每个进程独立运行,相互隔离,一个进程的崩溃不会影响其他进程。这对于短服务来说是足够的,因为每个连接的处理时间短,出错概率低。
(二)多线程模式
适用场景 :多线程模式适用于短服务,但也可以用于长服务。
原因 :
-
资源开销 :线程的资源开销比进程小,创建和销毁线程的速度比进程快。对于短服务,线程可以快速处理请求并关闭,不会占用太多资源。
-
并发能力 :线程可以共享进程的内存空间,减少了内存占用,提高了并发能力。对于长服务,线程可以持续处理多个连接,但需要考虑线程安全和资源竞争问题。
(三)线程池模式
适用场景 :线程池模式最适合长服务。
原因 :
-
资源管理 :线程池预先创建了一组线程,减少了线程创建和销毁的开销。对于长服务,连接的生命周期长,线程池可以高效地管理这些线程,提高资源利用率。
-
并发能力 :线程池可以限制线程的数量,避免过多的线程占用系统资源,导致系统性能下降。对于长服务,线程池可以持续处理多个连接,提供稳定的并发处理能力。
-
线程安全 :线程池中的线程共享任务队列,需要使用同步机制来保证线程安全。对于长服务,这种同步机制可以有效避免资源竞争和数据不一致问题。
三、未来应用占比与场景
(一)短服务
未来应用占比 :随着 Web 技术的发展,短服务仍然会占据一定的市场份额,但其增长速度可能会放缓。例如,传统的 Web 浏览和简单的 API 调用仍然会使用短服务。
适用场景 :
-
Web 浏览 :用户访问网页时,浏览器向服务器发送请求,服务器返回网页内容后关闭连接。
-
简单的 RESTful API :客户端发送请求,服务器处理请求并返回结果,然后关闭连接。
(二)长服务
未来应用占比 :随着实时通信、物联网和在线游戏等领域的快速发展,长服务的市场需求将不断增加。例如,即时通讯、物联网设备监控和在线游戏等场景对长服务的需求将持续增长。
适用场景 :
-
即时通讯 :如 QQ、微信等,客户端与服务器之间需要实时传输消息。
-
在线游戏 :服务器需要实时处理玩家的操作和反馈。
-
物联网设备监控 :设备与服务器之间需要持续传输数据。
多进程模式 :适用于短服务,资源开销较大,但隔离性好。
多线程模式 :适用于短服务,也可以用于长服务,但需要考虑线程安全和资源竞争问题。
线程池模式 :最适合长服务,资源管理高效,并发能力强,线程安全机制完善。
附录:源代码(缺少的代码在前面篇章中)
TcpServer.hpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;
// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port, func_t func) : _port(port),
_listensockfd(defaultsockfd),
_isrunning(false),
_func(func)
{
}
TcpServer(uint16_t port) : _port(port),
_listensockfd(defaultsockfd),
_isrunning(false)
{
}
void Init()
{
// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法
// 1. 创建套接字文件
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3
// 2. bind众所周知的端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3
// 3. 设置socket状态为listen
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3
}
class ThreadData
{
public:
ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
{
}
public:
int sockfd;
InetAddr addr;
TcpServer *tsvr;
};
// 短服务
// 长服务: 多进程多线程比较合适
void Service(int sockfd, InetAddr &peer)
{
char buffer[1024];
while (true)
{
// 1. 先读取数据
// a. n>0: 读取成功
// b. n<0: 读取失败
// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipe
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// buffer是一个英文单词 or 是一个命令字符串
buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1
LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;
std::string echo_string = _func(buffer, peer);
// // 2. 写回数据
// std::string echo_string = "echo# ";
// 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)
{
// a. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
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();
// version2: 多线程版本
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
// version0 -- test version --- 单进程程序 --- 不会存在的!
// Service(sockfd, addr);
// version1 --- 多进程版本
// pid_t id = fork(); // 父进程
// if(id < 0)
// {
// LOG(LogLevel::FATAL) << "fork error";
// exit(FORK_ERR);
// }
// else if(id == 0)
// {
// // 子进程,子进程除了看到sockfd,能看到listensockfd吗??
// // 我们不想让子进程访问listensock!
// close(_listensockfd);
// if(fork() > 0) // 再次fork,子进程退出
// exit(OK);
// Service(sockfd, addr); // 孙子进程,孤儿进程,1, 系统回收我
// exit(OK);
// }
// else
// {
// //父进程
// close(sockfd);
// //父进程是不是要等待子进程啊,要不然僵尸了??
// pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
// (void)rid;
// }
// // version2: 多线程版本
// ThreadData *td = new ThreadData(sockfd, addr, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, Routine, td);
// version3:线程池版本,线程池一般比较适合处理短服务
// 将新链接和客户端构建一个新的任务,push线程池中
// ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){
// this->Service(sockfd, addr);
// });
}
_isrunning = false;
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listensockfd; // 监听socket
bool _isrunning;
func_t _func; // 设置回调处理
};
TcpClient.cc
#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. bind吗??需要。显式的bind?不需要!随机方式选择端口号
// 2. 我应该做什么呢?listen?accept?都不需要!!
// 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;
}
TcpServer.cc
#include "Command.hpp"
#include "TcpServer.hpp"
#include "Dict.hpp"
std::string defaulthandler(const std::string &word, InetAddr &addr)
{
LOG(LogLevel::DEBUG) << "回调到了defaulthandler";
std::string s = "haha, ";
s += word;
return s;
}
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
// 远程命令执行的功能!
// ./tcpserver port
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();
// 1. 翻译模块
// Dict d;
// d.LoadDict();
// 1. 命令的执行模块
Command cmd;
// std::string Execute(const std::string &cmd, InetAddr &addr)
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,
std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));
// std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&cmd](const std::string &command, InetAddr &addr)
// { return cmd.Execute(command, addr); });
// std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&d](const std::string &word, InetAddr &addr)
//{ return d.Translate(word, addr); });
// 2. 启动服务器
tsvr->Init();
// 3. 运行服务器
tsvr->Run();
return 0;
}