在前面的文章中,我们使用了UDP进行网络编程,这篇文章我们就来使用另一个TCP进行网络编程,我们知道UDP和TCP都是传输层协议,但是特点不同,前者无连接,不可靠传输,面向数据报,后者有连接,可靠传输,面向字节流
首先,在之前的UDP网络编程中,我们是直接使用的硬编码,例如退出码直接就设为1、2、3等,显然这并不是一个很好的选择,那么这里我们可以统一设计一个服务器的退出码,就像之前设计日志等级一样,使用枚举常量
我们在Common.hpp文件中定义枚举常量
enum ExitCode
{
OK = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR
};在后续使用中,我们就能明白这些错误码的含义
另外,我们服务器通常是不能被拷贝的,那我们可以在Common.hpp文件中定义一个不能被拷贝的基类,后面不同的服务器都可以继承这个基类来达到不能被拷贝的目的
class NoCopy
{
public:
NoCopy(){}
~NoCopy(){}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator = (const NoCopy&) = delete;
};TcpServer.cc:
#include <memory>
#include "TcpServer.hpp"
#include "Common.hpp"
using task_t = std::function<void()>;
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
// ./udpserver 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();
// 网络服务器对象提供网络通信功能
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Run();
return 0;
}TcpServer.hpp:
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port)
:_port(port)
{}
void Init()
{}
void Run()
{}
~TcpServer() {}
private:
uint16_t _port; // 端口号
};第一步肯定是创建套接字,不过与UDP不同的是第二个参数,我们套接字类型选择面向连接的可靠字节流
const static int defaultsockfd = -1;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port)
:_port(port)
,_sockfd(defaultsockfd)
{}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void Run()
{}
~TcpServer() {}
private:
uint16_t _port; // 端口号
int _sockfd;
};注意:对于使用过的系统调用我们不再详细介绍,可翻看之前UDP网络编程
第二步就是绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);我们现在不需要再自己去填写地址结构的信息,因为我们之前封装了 InetAddr 类,我们直接在 InetAddr 类中再实现一个获取地址结构的函数即可
const struct sockaddr* NetAddrPtr()
{
return &_addr;
}由于我们参数中地址结构的类型是 const struct sockaddr ,但是 InetAddr 类中_addr 是 const struct sockaddr_in 类型,所以我们需要类型转换一下
所以我们在Common.hpp文件中实现一个类型转换的宏
#define CONV(addr) ((const struct sockaddr*)&addr)下面就直接在返回时使用这个宏就可以了
const struct sockaddr* NetAddrPtr()
{
return CONV(_addr);
}bind中第三个参数需要知道地址结构的长度,同样 InetAddr 类中再增加一个获取地址结构长度的函数
socklen_t NetAddrLen()
{
return sizeof(_addr);
}另外,我们服务端需要监听服务器的所有IP,任何网络接口(网卡)发来的连接我们都愿意接受,所以我们地址结构中的成员 sin_addr 需要设置为 INADDR_ANY,也就是IP地址此时为0,这在UDP网络编程时我们已经详细介绍了原因,这意味着我们服务端在使用 InetAddr 类时只需要传入端口号,那么我们构造函数还需要重载一个供服务端使用
InetAddr(uint16_t port)
:_ip("0"), _port(port)
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_addr.sin_port = htons(_port);
}那绑定地址就简单了,代码如下:
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
// 2. 绑定地址
InetAddr local(_port);
int n = bind(_sockfd, local.NetAddrPtr(), local.NetAddrLen());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}第三步与UDP编程不同,我们TCP在绑定之后需要listen
为什么UDP不需要listen,而TCP就需要listen呢?
最根本的区别:面向连接 与 无连接。
TCP 的工作方式:为什么需要 listen
TCP的通信过程就像打电话,有一套严格的礼仪。
listen 的核心作用就是创建那个“等待接听的电话队列”。没有 listen,即使客户端尝试连接,服务器的操作系统也不知道该如何处理这个连接请求,会直接拒绝(RST包)。
注意:这里涉及到的三次握手等,我们后面在介绍传输层TCP协议时会详细介绍这些相关内容,目前我们暂只需要学会TCP网络编程的相关系统调用,后续会慢慢介绍其它
UDP 的工作方式:为什么不需要 listen
UDP的通信过程就像寄明信片。
UDP 没有“连接”的概念,因此:
内核层面的视角
listen系统调用
listen 系统调用将一个已绑定的套接字置于“被动监听”状态,使其能够接受来自客户端的连接请求。它本身并不接受连接,而是为后续的 accept 调用做准备,并设置连接请求队列的长度。
简单来说,它的作用是:“告诉操作系统,我这个套接字已经准备好接受连接了,如果有客户端来连接,请先把它们安排在这个队列里。”
#include <sys/socket.h>
int listen(int sockfd, int backlog);参数详解
int sockfdint backlog返回值
EBADF: sockfd 不是有效的文件描述符。
EINVAL: 套接字未调用 bind 进行绑定,或者该套接字不是 SOCK_STREAM 类型。
ENOTSOCK: sockfd 不是一个套接字描述符。
这里我们自己设置一个
const static int backlog = 8;代码如下:
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
// 2. 绑定地址
InetAddr local(_port);
int n = bind(_sockfd, local.NetAddrPtr(), local.NetAddrLen());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
// 3. 设置socket状态为listen
n = listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _sockfd;
}还是和之前一样使用运行标志位来表示运行状态
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port)
:_port(port)
,_sockfd(defaultsockfd)
,_isrunning(false)
{}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
// 2. 绑定地址
InetAddr local(_port);
int n = bind(_sockfd, local.NetAddrPtr(), local.NetAddrLen());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
// 3. 设置socket状态为listen
n = listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _sockfd;
}
void Run()
{
_isrunning = true;
while(_isrunning)
{
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port; // 端口号
int _sockfd;
bool _isrunning;
};accept 系统调用从已完成连接队列中取出第一个连接请求,创建一个新的套接字用于与客户端通信,并返回这个新套接字的文件描述符。
核心理解:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数详解
int sockfdstruct sockaddr *addrsocklen_t *addrlen返回值
我们socket创建套接字不是已经返回一个 “文件描述符” 了嘛,为什么accept也会返回一个 “文件描述符” 呢?两者有什么区别吗?
想象一个银行:
银行大门(监听套接字)
柜台窗口(连接套接字)
技术层面:
关键区别
特性 | socket() 返回的描述符 | accept() 返回的描述符 |
|---|---|---|
角色 | 监听器(接受新连接) | 连接端点(与客户端通信) |
数量 | 通常一个 | 每个客户端连接一个 |
数据传递 | 不直接传输数据 | 直接读写数据 |
关联对象 | 服务器地址和端口 | 特定客户端的 IP 和端口 |
为什么需要两个描述符?
这种设计实现了并发处理:服务器用一个监听描述符持续接受新请求,同时为每个已连接客户端创建独立的描述符处理数据交换,互不干扰。
所以socket返回的是监听套接字,那我们可以将成员变量_sockfd修改为_listensockfd增加代码可读性,然后accept接受客户端的连接
代码如下:
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. 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
// 2. 绑定地址
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";
// 3. 设置socket状态为listen
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
}
void Run()
{
_isrunning = true;
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port; // 端口号
int _listensockfd;
bool _isrunning;
};接受客户端的连接后,就可以对客户端发送给服务端的数据进行处理,我们可以先来写一个简单的EchoServer服务
和UDP一样的步骤,我们需要先读取客户端发送的数据,然后再写回,因为tcp已经和客户端建立好连接了,所以不需要和UDP一样每次收发数据都需要完整的地址信息,而且tcp和文件操作一样,都是面向字节流的,所以我们可以使用read/write来读写数据
void Service(int sockfd, InetAddr& addr)
{
char buffer[1024];
while(true)
{
// 1. 读取数据
// a. n>0: 读取成功
// b. n<0: 读取失败
// c. n==0: 对端把链接关闭了,读到了文件的结尾
ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG) << addr.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) << addr.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel::DEBUG) << addr.StringAddr() << "异常...";
close(sockfd);
break;
}
}
}
void Run()
{
_isrunning = true;
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
// v0——EchoServer
Service(sockfd, addr);
}
_isrunning = false;
}但是这里有个问题,当一个客户与服务端连接后进行读写数据时,此时服务端就会执行Service函数,但是这个时候如果再来一个或多个客户与服务端进行连接时,服务端是不能accept连接客户端的,因为服务端是单进程在执行Service函数,也就是只要当前客户与服务端的连接没有断开,那么服务端就会一直死循环进行收发数据,所以其他客户就不能与服务端建立连接
那要怎么做呢?
我们可以使用多进程,创建一个子进程去执行任务,父进程则不断与客户端建立连接
问题1:进程如果退出了,曾经打开的文件会怎么办?
默认会被自动释放掉,fd,会自动被关闭,close(fd)
问题2:进程如果打开了一个文件,得到了一个fd,如果在创建子进程,这个子进程能拿到父进程的fd进行访问吗?
能,之前学习的管道不就是吗,fork创建子进程,然后分别关闭父子进程的读写端,这不就是子进程拿到了父进程的fd来进行访问吗
void Run()
{
_isrunning = true;
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
// v0——EchoServer
// Service(sockfd, addr);
// v1——EchoServer 多进程版
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if(id == 0)
{
// 子进程
// 我们不想让子进程访问listensock!
close(_listensockfd);
Service(sockfd, addr);
exit(OK);
}
else
{
// 父进程
close(sockfd);
//父进程是不是要等待子进程啊,要不然僵尸了??
pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?
(void)rid;
}
}
_isrunning = false;
}这里我们父进程需要等待子进程退出,要不然子进程会成为僵尸进程,可是我们这里是阻塞等待啊,那服务端还是不能去连接其他客户啊,这怎么办?
首先,我们在学习信号时,提到过子进程退出时会给父进程发送 SIGCHLD 信号,父进程可以通过捕获此信号来调用wait/waitpid回收子进程。
那这里推荐做法就是父进程可以显式忽略该信号,这样父进程就可以继续执行自己的任务,完全不需要调用wait/waitpid,因为OS内核会自己清理回收子进程资源
这里还有另一个推荐的做法,就是子进程再次fork创建子进程,然后子进程立即退出,留下孙子进程来执行任务,孙子进程此时会成为孤儿进程,由操作系统管理回收,那么父进程就不会再阻塞了,因为子进程已经退出了
void Run()
{
_isrunning = true;
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
// v0——EchoServer
// Service(sockfd, addr);
// v1——EchoServer 多进程版
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if(id == 0)
{
// 子进程
// 我们不想让子进程访问listensock!
close(_listensockfd);
if(fork() > 0) // 再次fork,子进程直接退出
exit(OK);
Service(sockfd, addr); // 孙子进程来执行任务,但是孙子进程会成为孤儿进程,由系统来回收
exit(OK);
}
else
{
// 父进程
close(sockfd);
//父进程是不是要等待子进程啊,要不然僵尸了??
pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
(void)rid;
}
}
_isrunning = false;
}那除了多进程,我们当然还可以使用多线程了,这里我们先使用原生的线程来实现多线程版
代码如下:
class ThreadData
{
public:
ThreadData(int fd, InetAddr &addr, TcpServer *tsvr)
:_sockfd(fd), _addr(addr), _tsvr(tsvr)
{
}
public:
int _sockfd;
InetAddr _addr;
TcpServer *_tsvr;
};
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(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
// v0——EchoServer
// Service(sockfd, addr);
// v1——EchoServer 多进程版
// pid_t id = fork();
// if(id < 0)
// {
// LOG(LogLevel::FATAL) << "fork error";
// exit(FORK_ERR);
// }
// else if(id == 0)
// {
// // 子进程
// // 我们不想让子进程访问listensock!
// close(_listensockfd);
// if(fork() > 0) // 再次fork,子进程直接退出
// exit(OK);
// Service(sockfd, addr); // 孙子进程来执行任务,但是孙子进程会成为孤儿进程,由系统来回收
// exit(OK);
// }
// else
// {
// // 父进程
// close(sockfd);
// //父进程是不是要等待子进程啊,要不然僵尸了??
// pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
// (void)rid;
// }
// v2——EchoServer 多线程版
ThreadData* td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
_isrunning = false;
}这里类内成员函数隐含this指针,所以要使用静态成员函数,但静态成员函数不能访问非静态成员变量,所以我们使用了一个内部类 ThreadData ,并且传入this指针,方便我们使用类内成员变量,这些我们在学习线程时也介绍过,就不再多做解释
同时我们线程也需要等待,那要等待的话不就又会阻塞在这里了,所以我们在创建线程时就分离线程,就不需要等待线程了
说到多线程,当然就能想到线程池,所以我们还可以实现一个线程池版
#pragma once
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include "Common.hpp"
#include <sys/wait.h>
#include <pthread.h>
using namespace LogModule;
using namespace ThreadPoolModule;
using task_t = std::function<void()>;
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. 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
// 2. 绑定地址
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";
// 3. 设置socket状态为listen
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& addr)
{
char buffer[1024];
while(true)
{
// 1. 读取数据
// a. n>0: 读取成功
// b. n<0: 读取失败
// c. n==0: 对端把链接关闭了,读到了文件的结尾
ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG) << addr.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) << addr.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel::DEBUG) << addr.StringAddr() << "异常...";
close(sockfd);
break;
}
}
}
class ThreadData
{
public:
ThreadData(int fd, InetAddr &addr, TcpServer *tsvr)
:_sockfd(fd), _addr(addr), _tsvr(tsvr)
{
}
public:
int _sockfd;
InetAddr _addr;
TcpServer *_tsvr;
};
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(peer);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errpr";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();
// v0——EchoServer
// Service(sockfd, addr);
// v1——EchoServer 多进程版
// pid_t id = fork();
// if(id < 0)
// {
// LOG(LogLevel::FATAL) << "fork error";
// exit(FORK_ERR);
// }
// else if(id == 0)
// {
// // 子进程
// // 我们不想让子进程访问listensock!
// close(_listensockfd);
// if(fork() > 0) // 再次fork,子进程直接退出
// exit(OK);
// Service(sockfd, addr); // 孙子进程来执行任务,但是孙子进程会成为孤儿进程,由系统来回收
// exit(OK);
// }
// else
// {
// // 父进程
// close(sockfd);
// //父进程是不是要等待子进程啊,要不然僵尸了??
// pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
// (void)rid;
// }
// v2——EchoServer 多线程版
// ThreadData* td = new ThreadData(sockfd, addr, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, Routine, td);
// v3——EchoServer 线程池版
ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){
this->Service(sockfd, addr);
});
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port; // 端口号
int _listensockfd;
bool _isrunning;
};注意:我们线程池是固定5个线程,所以我们最多只有五个客户端与服务端进行连接
这是因为我们Service函数是在死循环执行,这种我们可以称之为长服务,与之对应,那肯定就还有短服务,什么意思呢?下面我们来认识一下短服务和长服务
一般多进程多线程比较适合长服务,线程池适合短服务,但也不是绝对的,这里我们只简单提一下,当然也可以将死循环改一下,那客户端数量就不会仅限于5个了,感兴趣可以自己下来尝试一下
客户端实现其实和udp网络编程时大差不差
代码如下:
#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[])
{
// 客户端需要绑定服务器的ip和port
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
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. 直接向目标服务器发起建立连接的请求
return 0;
}我们这里也不需要显式bind,关于原因我们在udp网络编程时已经说明了,那我们应该做什么呢?需要和服务端一样listen或者accept吗?不需要,因为服务端就相当于是接电话的人,所以服务端需要监听和接受连接,而我们客户端则相当于打电话的人,那肯定是需要向服务端发起连接,即需要connect
connect系统调用详解
connect 系统调用用于客户端向服务器发起连接请求(TCP)或设置默认对端地址(UDP)。
核心作用:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数详解
int sockfdconst struct sockaddr *addrsocklen_t addrlen返回值
代码如下:
#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[])
{
// 客户端需要绑定服务器的ip和port
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
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(server_ip, server_port);
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 << "sercer echo# " << buffer << std::endl;
}
}
return 0;
}运行结果:

我们网络服务已经完成了,上层服务我们可以和UDP网络编程一样,直接在服务端主程序调用其他的服务,就比如之前实现的翻译和路由转发,然后在服务端接收数据时,将数据回调处理,最后将结果写回客户端。不过这里我们就不实现了,因为和UDP是一样的。