一、TCP socket API
TCP编程与前面的UDP编程一样,都属于网络通信,所以也需要包含下面三个网络通信必备的头文件:

第一个接口socket()用于创建套接字:
cpp
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
与前面的UDP不同的地方在于,由于TCP协议是面向字节流的,所以类型type要采用SOCK_STREAM。
第二个接口bind()用于把socket文件描述符与本机地址进行绑定:
cpp
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
由于TCP是面向连接的,所以TCP编程需要比UDP多一个环节,就是客户端需要向服务器端发起连接请求。也就是说服务器端需要处于一种监听的状态(叫做listen),便于收到客户端的连接请求。所以也就需要使用到第三个接口listen:

listen的返回值:

listen接口的作用就是设置套接字sockfd为监听状态。此接口中的backlog表示的是链接队列的长度,即链接队列最大容纳连接数。它是一个整型数值(如 10、128)。这里我们将链接数量设置为16:
cpp
static const int gbacklog = 16;
n = listen(_listensockfd, gbacklog);
当完成这些工作后,就可以启动TCP服务了。查看我们所启动的TCP服务可以使用指令netstate -antp。-a表示查看所有的信息,-n表示1用数字显示,-t表示显示的是tcp的内容(-u表示的则是upd的内容),-p表示将进程信息带上。

在完成了上述的工作之后,我们接下来需要完成服务器的启动start();
与UDP不同的是,由于TCP是面向连接的,所以不能一上来就读取客户端的信息,先要做的是获取客户端的连接请求。获取连接我们需要使用的接口是accept():


上面这两个参数合起来可以帮助我们获取到客户端的socket ip+port信息。
cpp
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
这一步操作其实类似于UDP中recvfrom接口的作用,都是用于获取客户端的ip和端口号信息。

当获取失败,依旧是返回-1;但是当返回成功时,该接口将返回一个文件描述符。而在TCP中,与UDP不同的是,会存在很多的文件描述符,而我们之前所创建的文件描述符叫做listen_socket监听套接字。未来返回的套接字叫做IO_socket,即服务套接字。未来一个服务套件字代表一条连接1!
accept()用于接收连接,是服务器端对连接的操作;而用于客户端发起连接请求的接口是--connect():

连接成功返回0,失败返回-1。

在进行地址转换时会使用下面两个接口------inet_ntop,inet_pton:
inet_ntop

这个接口的作用是把二进制(网络字节序)IP 地址,转换成人可读的点分字符串格式
int af:地址族,AF_INET代表 IPv4,AF_INET6代表 IPv6
const void *src:输入二进制 IP(网络字节序),IPv4就传&(sockaddr_in)->sin_addr
char *dst:输出缓冲区,转换完成的 IP 字符串会写到这里,由调用者自己提供内存
socklen_t size:dst 缓冲区字节大小,防止缓冲区溢出
inet_pton

这个接口的作用是把点分字符串格式IP 地址,转换成二进制(网络字节序)IP地址。
int af:地址族,AF_INET(IPv4) / AF_INET6(IPv6)
const char *src:输入IP 字符串,如 "127.0.0.1"
void *dst:输出缓冲区,存放二进制网络序 IP。IPv4 传&addr.sin_addr
二、version0:单进程版本
version0仅用于做接口的说明和熟悉后续所要用的接口:
在成功获取到连接后,我们就可以处理新的socket套接字了
cpp
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
TCP和UDP一样,在进行通信时都是全双工的
cpp
void serviceIO(int socket,InetAddr &address)
{
LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
while(true)
{
char inbuffer[1024] = {0};
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());//写入
}
else if (n == 0)//读到0说明连接断开了
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
break;
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
break;
}
}
}
version0服务器端完整代码如下:
EchoTcpServer.hpp(version0)
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const int gbacklog = 16;
static const uint16_t gport = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = gport) : _port(port)
{
}
void InitServer()
{
// 1. 创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
// 2. 填充本地socket信息
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; // 任意地址bind
// 3. bind
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind socket success";
// 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen socket success";
}
void serviceIO(int socket,InetAddr &address)
{
LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
while(true)
{
char inbuffer[1024] = {0};
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());//写入
}
else if (n == 0)//读到0说明连接断开了
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
break;
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
break;
}
}
}
void Start()
{
while (true)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 5. 获取连接
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
//6. 处理新sockfd
version 0
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
// 不需要显示包含ip
int _listensockfd;
};
TCP的客户端与UDP的客户端的逻辑一模一样,要访问目标服务器,首先需要知道服务器端的ip
cpp
// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
接下来就是创建客户端的tcp socket套接字:
cpp
// 1. 创建tcpsocket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:网络通信服务,SOCK_STREAM:流式套接
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
然后是进行绑定操作bind。无论是UPD还是TCP,都需要绑定自己的ip号和端口号。但是都不需要自己手动地对客户端进行绑定,操作系统会自动为我们分配随机端口,防止端口号冲突!
但是TCP在这里比UDP多一步操作,由于TCP是面向连接的,所以在bind端口号和ip过后,需要发起建立连接的请求!所以我们在这里需要使用到接口connect()。
需要注意的是,connect()接口在建立连接时,会自动帮我们进行绑定,也就是说此处只需要只用到connect接口即可!
cpp
// 2. bind端口号ip,发起建立连接的工作
InetAddr serveraddress(server_port, server_ip);//构建好对象后InetAddr中会自动转换为网络结构
int n = connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len());
if (n < 0)
{
std::cerr << "connect to " << serveraddress.ToString() << " failed!"<< std::endl;
exit(3);
}
std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;
连接完成后就可以进行通信操作了:
cpp
// 3. 通信
while(true)
{
std::string line;
std::cout << "please Enter# ";
std::getline(std::cin, line);
write(sockfd, line.c_str(), line.size());
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));//同一个套接字既可以读又可以写,证明TCP通信是全双工的。
if(n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
else if(n == 0)
{
std::cout << "read enf of file!" << std::endl;
break;
}
else
{
std::cerr << "read error!" << std::endl;
break;
}
}
客户端完整代码如下:
cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
static void Usage(const std::string &name)
{
std::cerr << "Usage:\n\t";
std::cerr << name << " server_ip server_port" << std::endl;
}
// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建tcpsocket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 2. bind端口号ip,发起建立连接的工作
// {client ip, client port} <-> {server ip, server port}
InetAddr serveraddress(server_port, server_ip);
int n = connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len());
if (n < 0)
{
std::cerr << "connect to " << serveraddress.ToString() << " failed!"<< std::endl;
exit(3);
}
std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;
// 3. 通信
while(true)
{
std::string line;
std::cout << "please Enter# ";
std::getline(std::cin, line);
write(sockfd, line.c_str(), line.size());
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));
if(n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
else if(n == 0)
{
std::cout << "read enf of file!" << std::endl;
break;
}
else
{
std::cerr << "read error!" << std::endl;
break;
}
}
return 0;
}
cpp
#include "EchoTcpServer.hpp"
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port);
tsvr->InitServer();
tsvr->Start();
return 0;
}
编译运行后启动服务器端和客户端,再输入命令netstate -antp查看所启动的TCP服务,会发现server_tc和client_tc已经建立起了对应的连接。一条是客户端向服务器端建立的连接,一条是服务器端向客户端建立的连接。由于是在一台机器上启动的服务器端和客户端,所以相当于一台机器上会看到两个不同视角的连接,一个是客户端的,一个是服务器端的。

但是,上述version0是一个单进程的TCP服务,当一个客户端与服务器端建立起连接后,如果客户端一直不退出,那么进程就会阻塞在读写那部分逻辑中,也就是其他的客户端无法与服务器建立起连接,服务器无法并发地处理多个客户端发出的请求。这显然与当代网络通信的要求不符。
三、version1:多进程版本
为了解决上述问题,需要在version0的基础上进行改造,进而引申出版本version1。
单进程的服务器对客户端请求的处理显然行不通。于是有了version1版本,引入了父子进程。由父进程负责对客户端的请求的接收,然后将这些接收到的请求交给子进程,让子进程进程处理IO。
如果创建子进程失败,返回-1。如果创建子进程失败则后面的操作都无法进行下去,所以直接让服务器进程终止。
cpp
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork errr!";
exit(FORK_ERR);
}
创建进程成功后,让子进程进行IO任务的处理
cpp
else if(id == 0)
{
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
close(sockfd);
exit(0); // 终止子进程
}
当子进程完成任务之后,由于fork之后的代码是共享的,如果子进程不退出就有可能进执行父进程获取连接部分的代码,这样的情况显然不合理。所有当子进程完成任务之后,我们要直接终止子进程防止影响后续父进程的代码执行。
但是如果终止了子进程,那子进程就进入了僵尸状态。在前面的Linux进程中我们提到过,子进程终止需要父进程wait去回收子进程。但是如果按照这个逻辑来完成这部分的代码,代码的执行不就又变成串行了吗?父进程需要等待子进程退出,回收子进程后,才能执行后续任务。
或者采用在前面学到过的waitpid中的WNHANG信号,但是这种方法的调用时机、回收范围不对,依旧产生僵尸进程,从而产生bug。
所以,此处的最佳实践是加上signal(SIGCHLD, SIG_IGN),子进程一退出,会自动释放,无需父进程等待回收子进程。
在此处也有另外一种方式,那就是子进程再fork一次,创建孙子进程,然后子进程直接退出,让孙子进程来执行后续的IO任务。而由于子进程退出了,孙子进程就变成孤儿进程了。这是利用系统特性的写法。
父进程成功fork后创建出子进程,父进程的文件描述符表也要给子进程拷贝一份。但是在后续子进程的任务中并没有使用到监听套接字(listen_socket)。在网络通信中,有一个大的原则,那就是如果有一个资源不再使用了,那就需要尽早释放掉。文件描述符是有用且有限的资源,由于子进程在后续不再使用这部分资源,就需要及时释放掉。
同理,父进程只负责接收客户端的信号,所以不需要使用到服务套接字(IO_socket),所以也需要及时关闭这部分套接字:
cpp
else if(id == 0)
{
// 关闭掉自己不需要的sockfd
close(_listensockfd);
// 子进程
if(fork() > 0) exit(0); // 子进程直接退出
// 孙子进程 - 孤儿进程
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
close(sockfd);
exit(0); // 终止子进程
}
else
{
// 父进程
// wait(id); waitpid: WNOHANG
// 关闭掉自己不需要的sockfd
close(sockfd);
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
version1服务器端完整代码:
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
FORK_ERR
};
static const int gbacklog = 16;
static const uint16_t gport = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = gport) : _port(port)
{
}
void InitServer()
{
// 1. 创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
// 2. 填充本地socket信息
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; // 任意地址bind
// 3. bind
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind socket success";
// 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen socket success";
}
void serviceIO(int socket,InetAddr &address)
{
LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
while(true)
{
char inbuffer[1024] = {0};
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());//写入
}
else if (n == 0)//读到0说明连接断开了
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
break;
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
break;
}
}
}
void Start()
{
while (true)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 5. 获取连接
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
//version1:多进程版本
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork errr!";
exit(FORK_ERR);
}
else if(id == 0)
{
// 关闭掉自己不需要的sockfd
close(_listensockfd);
// 子进程
if(fork() > 0) exit(0); // 子进程直接退出
// 孙子进程 - 孤儿进程
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
close(sockfd);
exit(0); // 重要
}
else{
// 父进程
// wait(id); waitpid: WNOHANG
// 关闭掉自己不需要的sockfd
close(sockfd);
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
// 不需要显示包含ip
int _listensockfd;
};
这里介绍一个工具,用于网络编程测试服务器------telnet:
命令格式
bash
telnet 服务IP 端口号
连接成功后,按下Ctrl + ]调出命令行,键盘输入文字回车,会直接发给服务端。服务端返回的数据实时打印在终端。再按下Ctrl + ]调出命令行,输入quit回车将会断开连接。
四、version2:多线程版本
在前面的version1多进程服务器中,每有一个客户端就需要创建一个进程,这样的话未免开销太大。所以也就延伸出了version2多线程版本。在这里,我们直接使用C++库封装的线程。
需要包含头文件
cpp
#include<pthread.h>
cpp
//version 2:多线程版本
pthread_t tid;
InetAddr clientaddress(clientaddr);
ThreadData *td = new ThreadData(this, sockfd, clientaddress);
pthread_create(&tid, nullptr, thread_routine, (void *)td);
同理,由于主线程也需要等待回收线程join(),一旦join就会阻塞等待,这样不就出现和父子进程一样的问题了吗?所以我们要把线程设置为分离状态。
这里的thread_routine任务函数使用static静态方法,是为了避免this指针,保证参数传递的正确性。但是将线程函数中无法调用ServiceIO函数,因为pthread线程函数只能传一个 void* 参数,而使用ServiceIO函数需要传入多个参数,所以需要一个类来封装多组数据。
此处不需要像前面的父子进程一样关闭不需要的套接字,因为父子线程是共享文件描述符表的。
cpp
class ThreadData
{
public:
ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
: _this(ts), _sockfd(sockfd), _addr(addr)
{
}
~ThreadData()
{
close(_sockfd);
}
public:
TcpServer *_this;
int _sockfd;
InetAddr _addr;
};
static void *thread_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
pthread_detach(pthread_self());
td->_this->serviceIO(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
由于上述代码中服务器端提供的服务是长服务,长连接。而多进程、多线程的模式不太适用于处理这种长服务,长连接的服务,大多用于小型应用的服务。一旦有大量的客户端的请求涌入服务器,进程和线程数量大量增多,就会增加CPU的调度成本,进程和线程切换的成本增加,使得服务器卡顿。所以如果要使用多进程或多线程版本的服务器,我们就需要将长服务改为短服务。由此需要将代码中的长服务改为短服务(就是去掉while循环的逻辑)
所以version2服务器端完整代码为:
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
FORK_ERR
};
static const int gbacklog = 16;
static const uint16_t gport = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = gport) : _port(port)
{
}
void InitServer()
{
// 1. 创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
// 2. 填充本地socket信息
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; // 任意地址bind
// 3. bind
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind socket success";
// 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen socket success";
}
void serviceIO(int socket,InetAddr &address)
{
LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
//长服务改为短服务
char inbuffer[1024] = {0};
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());//写入
}
else if (n == 0)//读到0说明连接断开了
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
}
}
void Start()
{
while (true)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 5. 获取连接
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
//version 2:多线程版本
pthread_t tid;
InetAddr clientaddress(clientaddr);
ThreadData *td = new ThreadData(this, sockfd, clientaddress);
pthread_create(&tid, nullptr, thread_routine, (void *)td);
}
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
// 不需要显示包含ip
int _listensockfd;
};
五、version3:接入线程池
前面多线程的版本仍然有缺陷。原因是由于提供的服务是短服务,而每来一个短服务都需要创建一个线程,有可能创建线程的开销比短服务的开销还要大。所以我们可以直接接入线程池!这样就省去了来了一个客户端才创建一个线程,减少了大批量创建线程的开销。由此引出version3------线程池版本服务器。
version3服务器端完整代码为:
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"
using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
FORK_ERR
};
static const int gbacklog = 16;
static const uint16_t gport = 8888;
class TcpServer
{
public:
TcpServer(uint16_t port = gport) : _port(port)
{
}
void InitServer()
{
// 1. 创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
// 2. 填充本地socket信息
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; // 任意地址bind
// 3. bind
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind socket success";
// 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen socket success";
}
void serviceIO(int sockfd, InetAddr address)
{
// 短连接,短服务
char inbuffer[1024] = {0};
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;
// 写
std::string echo_string = "server echo# ";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
}
else
{
LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
}
close(sockfd);
}
class ThreadData
{
public:
ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
: _this(ts), _sockfd(sockfd), _addr(addr)
{
}
~ThreadData()
{
close(_sockfd);
}
public:
TcpServer *_this;
int _sockfd;
InetAddr _addr;
};
static void *thread_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
pthread_detach(pthread_self());
td->_this->serviceIO(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
void Start()
{
while (true)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 5. 获取连接
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
// version 3: 接入线程池
InetAddr clientaddress(clientaddr);
ThreadPool<task_t>::Instance()->Enqueue([this, sockfd, clientaddress]()->void{
this->serviceIO(sockfd, clientaddress);
});
}
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
// 不需要显示包含ip
int _listensockfd;
};
接入线程池的好处在于线程池会预先创建固定上限的工作线程,不会随客户端并发请求无限制新建线程。当大量客户端同时接入、空闲线程耗尽时,新任务会存入任务队列排队等待,避免瞬间大批量创建线程耗尽系统资源,导致服务器卡顿、崩溃。
六、其他代码
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
{
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // h->n
_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
_len = sizeof(_address);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
InetAddr()
{}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
在上面的地址转换中,inet_ntoa内部使用静态全局缓冲区,在多线程环境下存在线程安全问题,多线程同时调用会覆盖数据。所以需要做适当修改:
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
{
// _ip = inet_ntoa(_address.sin_addr);
char ipstr[32];
inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
_ip = ipstr;
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // h->n
//_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
inet_pton(AF_INET, ip.c_str(), &(_address.sin_addr));
_len = sizeof(_address);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
InetAddr()
{}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
Makefile
bash
.PHONY:all
all:client_tcp server_tcp
server_tcp:EchoTcpServerMain.cc
g++ -o $@ $^ -std=c++17 -lpthread
client_tcp:EchoTcpClient.cc
g++ -o $@ $^ -std=c++17 -static
.PHONY:clean
clean:
rm -f client_tcp server_tcp
以下略(往期博客中有):
Cond.hpp
Mutex.hpp
Thread.hpp
ThreadPool.hpp
Logger.hpp