「前言」文章是关于网络编程的socket套接字方面的,上一篇是网络编程socket套接字(二),下面开始讲解!
「归属专栏」网络编程
「笔者」枫叶先生(fy)
「座右铭」前行路上修真我
「枫叶先生有点文青病」「每篇一句」
I do not know where to go,but I have been on the road.
我不知道将去何方,但我已在路上。
——宫崎骏《千与千寻》
目录
四、 简单的TCP网络程序
4.1 服务端创建
4.1.1 创建套接字
4.1.2 绑定端口
4.1.3 监听
4.1.4 获取新链接
4.1.5 服务端代码(版本一)
4.2 客户端创建
4.2.1 创建套接字
4.2.2 连接服务器
4.2.3 客户端代码
4.3 服务端和客户端测试
4.4 多进程版的TCP网络程序
4.4.1 方法一:捕捉SIGCHLD信号
4.4.2 方法二:孙子进程
4.5 多线程版的TCP网络程序
4.6 线程池版的TCP网络程序
首先回顾一下TCP的特点:
注:到TCP原理再详细解释这些特点,这里只是简单了解
接下来进行编写socket套接字代码,使用的是TCP,也是边写代码边讲一下TCP的接口,还有一些原理。
首先明确,这个简单的TCP网络程序分客户端和服务端,所以我们要生成两个可执行程序,一个是客户端的,另一个是服务端的,服务端充当的是服务器,暂时实现的功能是客户端和服务端简单进行通信,服务端要可以收到客户端发送给服务端的信息,并把消息回显给客户端,目前就先简单实现这样的功能
下面进行编写服务端的代码
创建套接字的函数是socket,TCP/UDP 均可使用该函数进行创建套接字,该函数在上一篇编写UDP代码的时候已经详细介绍过了,这里就不再介绍,直接使用
int socket(int domain, int type, int protocol);
服务端创建套接字编写代码暂时如下:
tcpServer.hpp
#pragma once
#include
#include
#include
#include
using namespace std;
// 错误类型枚举
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _sockfd(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << _sockfd << endl;
}
// 启动服务器
void start()
{}
~tcpServer()
{}
private:
int _sockfd; // 文件描述符
uint16_t _port; // 端口号
};
注:创建套接字失败,没必要继续执行代码了,直接退出程序即可
tcpServer.cc
#include "tcpServer.hpp"
#include
// 使用手册
// ./tcpServer port
static void Uage(string proc)
{
cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uage(argv[0]);
exit(UAGE_ERR);
}
uint16_t port = atoi(argv[1]); // string to int
unique_ptr tsvr(new tcpServer(port));
tsvr->initServer(); // 初始化服务器
tsvr->start(); // 启动服务器
return 0;
}
注:
TCP 也是需要绑定端口号的,绑定端口号的函数是bind,TCP/UDP 均可使用进行该函数绑定端口
该函数在上一篇编写UDP代码的时候已经详细介绍过了,这里就不再介绍,直接使用
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
注:当定义好 sockaddr_in 结构体后,最好先用memset函数对该结构体进行清空,也可以用bzero函数进行清空。bzero函数也可以对特定的一块内存区域进行清空,bzero函数的函数原型如下:
void bzero(void *s, size_t n);
bzero()函数将从s开始的区域的前n个字节设置为零(包含“\0”的字节)。
头文件是:
#include
注:绑定失败,就直接退出程序了,不必要再执行
服务端绑定编写代码如下:
tcpServer.hpp
// 初始化服务器
void initServer()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "create socket success: " << _sockfd << endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
cout << "bind socket success" << endl;
}
tcpServer.cc 没有改变
注意:设置服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置 INADDR_ANY (全0),这个问题在上一节已经谈过
说一下前面UDP发送和接收的问题
因为UDP是面向数据报的,所以要用特定的接口进行收发消息。
为什么发送没有进行主机序列转为网络序列,接收消息没有把网络序列转为主机序列??
因为 recvfrom 和 sendto 是系统调用,这两个函数在函数内部已经帮我们做了,即主机序列转为网络序列和网络序列转为主机序列的工作
UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信
因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态
listen函数
listen函数的作用是设置套接字为监听状态, man 2 listen查看:
listen for connections on a socket:监听套接字上的连接
函数:listen
头文件:
#include
#include
函数原型:
int listen(int sockfd, int backlog);
参数:
第一个参数sockfd:需要设置为监听状态的套接字对应的文件描述符
第二个参数backlog:全连接队列的最大长度
返回值:
成功返回0,失败返回-1,同时错误码会被设置
注意:
TCP服务器在创建完套接字和绑定后,需要再进一步将套接字设置为监听状态,监听是否有新的连接到来。如果监听失败也没必要进行后续操作了,因为监听失败也就意味着TCP服务器无法接收客户端发来的连接请求,因此监听失败我们直接终止程序即可
服务监听代码编写如下:
tcpServer.hpp
static const int gbacklog = 5;
// 初始化服务器
void initServer()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "create socket success: " << _sockfd << endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
cout << "bind socket success" << endl;
// 3. 把_sockfd套接字设置为监听状态
if(listen(_sockfd, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
cout << "listen socket error" << endl;
}
tcpServer.cc 没有变化
在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成
上面的代码已经把服务器初始化完成了,客户端有新链接到来,服务端可以获取到新链接,这一步需要死循环获取客户端新链接
获取新链接的函数是 accept
accept函数
accept 函数的作用是用于获取客户端的链接,man 2 accept查看:
ccept a connection on a socket:接受套接字上的连接
函数:accept
头文件:
#include
#include
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
第一个参数sockfd:从该监听套接字中获取连接
第二个参数addr:对方一端网络相关的属性信息
第三个参数addrlen:addr的长度
返回值:
获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置
accept函数返回值问题
accept获取连接成功返回接收到的套接字的文件描述符
问题:为什么又返回一个新的文件描述符??返回的这个新的文件描述符跟旧的文件描述符_sockfd有什么关系??
下面用一个小栗子进行说明,比如有一家小餐厅,张三是站在店外面招揽客人的,有客人的到来张三就说:我们这店是这里最好的,您要进来吃吗?有的客人进去的,有的客人没进去。而进去的客人,店里面又会叫服务员A、B、C...进行一对一服务。而张三并没有进去,依旧是在外面招揽客人
listen监听套接字与accept函数返回的套接字的作用(新创建的文件描述符):
所以代码中的 _sockfd 套接字全部改为 _listensock
服务获取新链接代码编写如下:
未来通信就使用sockfd
tcpServer.hpp
// 启动服务器
void start()
{
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success" << endl;
}
}
服务端在获取连接时需要注意:accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接
tcpServer.cc 没有变化
接下来就是补充完整服务端的代码了,收取客户端发来的消息,并回显给客户端
后序通信就使用sockfd,而这个sockfd是面向字节流的,也就意味后序的操作全部是文件操作,也就是进行文件读写
服务端代码如下:
tcpServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
using namespace std;
static const int gbacklog = 5;
// 错误类型枚举
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "create socket success: " << _listensock << endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
cout << "bind socket success" << endl;
// 3. 把_listensock套接字设置为监听状态
if (listen(_listensock, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
cout << "listen socket success" << endl;
}
// 启动服务器
void start()
{
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 为sockfd提供服务,即为客户端提供服务
serviceIo(sockfd);
// 走到这里。服务已经提供完成,必须关闭 sockfd
close(sockfd);
}
}
// 提供服务
void serviceIo(int sockfd)
{
char buffer[1024];
while (true)
{
// 读取客户端发来的消息
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << "recv a message: " << buffer << endl;
// 回显消息给客户端
string outbuffer = buffer;
outbuffer += "server[echo]";
write(sockfd, outbuffer.c_str(), outbuffer.size());
}
else if (n == 0) // 客户端退出
{
cout << "client qiut, me too!" << endl;
break;
}
}
}
~tcpServer()
{}
private:
int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
uint16_t _port; // 端口号
};
tcpServer.cc 没有变化
注意
服务端已经可以编译运行了,运行结果如下:
netstat -atlp 查看
服务器已经处于监听状态了
netstat -atlpn 查看,n 以数字显示
注意:这个服务端读取是有问题的,后序再谈,目前就把内容当作字符串读取,也能满足目前需求
客户端的功能是可以发送消息给服务端,并收到服务端回显的消息,目前就先简单实现这样的功能
客户端也是使用socket函数创建套接字,与TCP服务端一样
客户端创建套接字代码编写如下:
tcpServer.hpp
class tcpClient
{
public:
tcpClient(const string &serverip, const uint16_t serverport)
: _serverip(serverip), _serverport(serverport), _sockfd(-1)
{
}
// 初始化客户端
void initClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
cerr << "socket create error" << endl;
exit(2);
}
// 2.绑定
// 客户端必须要进行bind绑定,但是不需要我们自己bind,OS帮我们完成
// 3.listen
// 客户端不需要listen
// 4. accept
//客户端不需要
}
// 启动客户端
void start()
{
//5. 客户端需要发起链接,链接服务端
}
~tcpClient()
{
}
private:
uint16_t _serverport; // 端口号
string _serverip; // ip地址
int _sockfd; // 文件描述符
};
注意:
tcpServer.cc
#include "tcpClient.hpp"
#include
// 使用手册
// ./tcpClient ip port
static void Uage(string proc)
{
cout << "\nUage:\n\t" << proc << " server_ip server_port\n\n";
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Uage(argv[0]);
exit(1);
}
// 客户端需要服务端的 IP 和 port
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]); // string to int
std::unique_ptr tcli(new tcpClient(serverip, serverport));
tcli->initClient(); // 初始化服务器
tcli->start(); // 启动服务器
return 0;
}
由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以向服务端发起连接请求。
connect函数
connect函数用于发起连接请求,man 2 connect查看:
initiate a connection on a socket:在套接字上启动连接
函数:connect
头文件:
#include
#include
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
第一个参数sockfd:表示通过该套接字发起连接请求
第二个参数addr:对方一端网络相关的属性信息
第三个参数addrlen:addr的长度
返回值:
连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置
客户连接代码编写如下:
tcpServer.hpp
void start()
{
// 5. 客户端需要发起链接,链接服务端
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport); // 主机转网络序列
server.sin_addr.s_addr = inet_addr(_serverip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
{
cerr << "socket connect error" << endl;
}
else // 连接成功
{
}
}
tcpServer.cc 没有变化
接下来就是补充完整服务端的代码了,客户端可以发送消息给服务端,客户端并且可以接收服务端回显的消息
后序操作也全部是文件操作
客户端代码如下:
tcpServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
static const int gnum = 1024;
class tcpClient
{
public:
tcpClient(const string &serverip, const uint16_t serverport)
: _serverip(serverip), _serverport(serverport), _sockfd(-1)
{}
// 初始化客户端
void initClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd == -1)
{
cerr << "socket create error" << endl;
exit(2);
}
// 2.绑定
// 客户端必须要进行bind绑定,但是不需要我们自己bind,OS帮我们完成
// 3.listen
// 客户端不需要listen
// 4. accept
// 客户端不需要
}
// 启动客户端
void start()
{
// 5. 客户端需要发起链接,链接服务端
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport); // 主机转网络序列
server.sin_addr.s_addr = inet_addr(_serverip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
{
cerr << "socket connect error" << endl;
}
else // 连接成功
{
string message;
while (true)
{
// 发送消息
cout << "Enter# ";
getline(cin, message);
write(_sockfd, message.c_str(), message.size());
// 接收服务端回显的消息
char buffer[gnum];
int n = read(_sockfd, buffer, sizeof(buffer) - 1);
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << "Server回显# " << buffer << endl;
}
else // 读取出错
{
break;
}
}
}
}
~tcpClient()
{}
private:
uint16_t _serverport; // 端口号
string _serverip; // ip地址
int _sockfd; // 文件描述符
};
tcpServer.cc 没有变化
注意:这个服务端读取是有问题的,后序再谈,目前就把内容当作字符串读取,也能满足目前需求
然后进行整体编译,编译没有问题
Makefile
新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试
其中文件描述符会随着客户端的链接而递增
发送消息测试,测试正常
查看一下进程信息
netstat -atlp 查看
客户端和服务端在同一台机器上跑,就会查到三个:
如果是两台主机,在服务端就会查到两个信息:
如果是两台主机,在客户端就会查到一个信息:
测试,客户端关闭,服务端相应的文件描述符也要随之关闭
该服务器的弊端
当我们仅用一个客户端连接服务端时,这一个客户端能够正常享受到服务端的服务
但在这个客户端正在享受服务端的服务时,我们让另一个客户端也连接服务器,此时虽然在客户端显示连接是成功的,但这个客户端发送给服务端的消息既没有在服务端进行打印,服务端也没有将该数据回显给该客户端
只有当第一个客户端退出后,服务端才会将第二个客户端发来是数据进行打印,并回显该第二个客户端。
通过实验现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器,这个服务器一次只能为一个客户端提供服务。
当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端
客户端为什么会显示连接成功?
当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上
实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。
如何解决这个问题
单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的,即多进程或多线程
客户端没有变化,要改的是服务端代码
把当前的单执行流服务器改为多进程版的服务器
等待子进程问题
当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏
阻塞式等待与非阻塞式等待子进程:
不等待子进程退出的方式
常见的方式有两种:
SIGCHLD信号是在子进程退出时由内核发送给父进程的信号。默认情况下,父进程会等待子进程退出并进行处理。但是,可以通过捕捉SIGCHLD信号并将其处理动作设置为忽略,来实现父进程不等待子进程退出的效果,这样父进程就不必关心子进程了
服务端代码修改如下
只需要更改start函数
// 启动服务器
void start()
{
// 忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 为sockfd提供服务,即为客户端提供服务
// serviceIo(sockfd);
// // 走到这里。服务已经提供完成,必须关闭 sockfd
// close(sockfd);
// 多进程版(忽略信号)
pid_t id = fork();
if (id == 0) // 子进程
{
close(_listensock);
serviceIo(sockfd);
close(sockfd);
exit(0);
}
// 父进程无需等待
}
}
编译没有问题
新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试
这样服务端就可以响应多个客户端的请求
注:读取可能会出现问题,这个后序再谈
把客户端关掉,重新连接,文件描述符正常关闭
查看一下进程信息
ps axj | head -1 && ps axj | grep tcpServer | grep -v grep
这两个客户端分别由两个不同的执行流提供服务,因此这两个客户端可以同时享受到服务,它们发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应
当客户端全部退出后,在服务端对应为之提供服务的子进程也会相继退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接
让父进程创建子进程,子进程再创建孙子进程,最后让孙子进程为客户端提供服务
对于父进程来说,子进程创建的进程与父进程的关系是孙子关系(站在父进程的视角)
由于子进程进程创建完孙子进程后就立刻退出了,因此实际为客户端提供服务的孙子进程就变成了孤儿进程,该进程就会被OS领养,当孙子进程为客户端提供完服务退出后OS会回收孙子进程,所以父进程是不需要等待孙子进程退出的
子进程需要关闭对应不用的文件描述符,否则就会造成文件描述符泄漏,或者子进程可能会对不需要的文件描述符进行某种误操作
父进程也需要关掉不用的文件描述符,否则就会造成文件描述符泄漏
服务端代码修改如下:
tcpServer.hpp
只需要更改start函数
// 启动服务器
void start()
{
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 为sockfd提供服务,即为客户端提供服务
// serviceIo(sockfd);
// // 走到这里。服务已经提供完成,必须关闭 sockfd
// close(sockfd);
// 多进程版(孙子进程)
pid_t id = fork();
if (id == 0) // 子进程
{
close(_listensock);
// 创建孙子进程,让子进程退出
if (fork() > 0)
exit(0);
// 孙子进程执行后序代码
serviceIo(sockfd);
close(sockfd);
exit(0);
}
// 父进程
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
cout << "wait success" << endl;
}
close(sockfd); // 必须关掉
}
}
编译没有问题
新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试
这样服务端就可以响应多个客户端的请求
注:读取可能会出现问题,这个后序再谈
把客户端关掉,重新连接,文件描述符正常关闭
查看一下进程信息
ps axj | head -1 && ps axj | grep tcpServer | grep -v grep
这两个客户端是由两个不同的孤儿进程提供服务的,因此它们也是能够同时享受到服务的,可以看到这两个客户端发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应。
PPID为1,表明这是一个孤儿进程
当客户端全部退出后,对应为客户端提供服务的孤儿进程也会跟着退出,这时这些孤儿进程会被系统回收,而最终剩下那个获取连接的父进程
注:如果服务端先退,客户端后退,下一次运行服务端就会绑定端口失败,换一个端口绑定就好了,至于原理后序再谈
频繁的创建进程会给OS带来巨大的负担,并且创建线程的成本比创建线程高得多。因此在实现多执行流的服务器时最好采用多线程进行实现。这块在多线程已经谈过,不再赘述
当服务进程调用accept函数获取到一个新连接后,就可以直接创建一个线程,让该线程为对应客户端提供服务。
主线程创建出新线程后,也是需要等待新线程退出的,否则也会造成类似于僵尸进程这样的问题。但对于线程来说,如果不想让主线程等待新线程退出,直接线程分离即可,当这个线程退出时系统会自动回收该线程所对应的资源。
主线程和新线程对文件描述符的态度
各个线程共享是同一张文件描述符表,也就是说服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是能够直接访问这个文件描述符的。
文件描述符关闭的问题
对于主线程accept上来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭
注意:线程的回调方法在类内需要设置为静态,至于原因在linux系统编程多线程已经解释过了,不再赘述
tcpServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
static const int gbacklog = 5;
// 错误类型枚举
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
class tcpServer; // 声明
class ThreadDate
{
public:
ThreadDate(tcpServer *self, int sockfd)
: _self(self), _sockfd(sockfd)
{}
public:
tcpServer *_self;
int _sockfd;
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "create socket success: " << _listensock << endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
cout << "bind socket success" << endl;
// 3. 把_listensock套接字设置为监听状态
if (listen(_listensock, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
cout << "listen socket success" << endl;
}
// 启动服务器
void start()
{
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 为sockfd提供服务,即为客户端提供服务
// serviceIo(sockfd);
// // 走到这里。服务已经提供完成,必须关闭 sockfd
// close(sockfd);
// 多线程版
pthread_t tid;
ThreadDate *td = new ThreadDate(this, sockfd);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 线程分离
ThreadDate *td = static_cast(args);
td->_self->serviceIo(td->_sockfd);
close(td->_sockfd); // 必须关闭,由新线程关闭
delete td;
return nullptr;
}
// 提供服务
void serviceIo(int sockfd)
{
char buffer[1024];
while (true)
{
// 读取客户端发来的消息
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0) // 读取成功
{
buffer[n] = 0;
cout << "recv a message: " << buffer << endl;
// 回显消息给客户端
string outbuffer = buffer;
outbuffer += " server[echo]";
write(sockfd, outbuffer.c_str(), outbuffer.size());
}
else if (n == 0) // 客户端退出
{
cout << "client qiut, me too!" << endl;
break;
}
}
}
~tcpServer()
{}
private:
int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
uint16_t _port; // 端口号
};
其他没有发送变化
注意:编译要带 -lpthread,因为使用了线程库
注意一下:前面客户端代码写漏了一个.... 刚发现。
编译没有问题
新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试
这样服务端就可以响应多个客户端的请求
把客户端关掉,重新连接,文件描述符正常关闭
查看一下线程信息
ps -aL
当客户端全部退出后,服务端的服务线程也随之退出
多线程存在的问题
解决方法:线程池
需要在服务端引入线程池,因为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价,此外,线程池还能够保证内核充分利用,防止过分调度
线程池在系统编程部分已经谈过,不再赘述
服务端代码编写如下:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace std;
static const int gbacklog = 5;
// 错误类型枚举
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
class tcpServer
{
public:
tcpServer(const uint16_t &port)
: _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cout << "create socket error" << endl;
exit(SOCKET_ERR);
}
cout << "create socket success: " << _listensock << endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
cout << "bind socket error" << endl;
exit(BIND_ERR);
}
cout << "bind socket success" << endl;
// 3. 把_listensock套接字设置为监听状态
if (listen(_listensock, gbacklog) == -1)
{
cout << "listen socket error" << endl;
exit(LISTEN_ERR);
}
cout << "listen socket success" << endl;
}
// 启动服务器
void start()
{
// 初始化线程池
unique_ptr> tp(new ThreadPool());
tp->run();
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
cout << "accept error, next!" << endl;
continue;
}
cout << "accept a new line success, sockfd: " << sockfd << endl;
// 5. 为sockfd提供服务,即为客户端提供服务
// 构建任务
tp->push(Task(sockfd, serviceIo));
}
}
~tcpServer()
{}
private:
int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
uint16_t _port; // 端口号
};
由于代码过多就不一一贴出来了,上传到Gitee
Gitee链接:code_linux/code_202306_16/2_tcp/4_tcpthpool · Maple_fylqh/code - 码云 - 开源中国 (gitee.com)
代码测试
编译没有问题
新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试
把客户端关掉,重新连接,文件描述符正常关闭
pa -aL 查看一下线程信息
运行服务端后,就算没有客户端发来连接请求,此时在服务端就已经有了6个线程,其中有一个是接收新连接的服务线程,而其余的5个是线程池当中为客户端提供服务的线程
与之前不同的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出,也就是说目前写的代码只适合用户访问量少。
写这个代码的目的是结合以前所学知识,对知识进行容纳贯穿
注: 内容过多,还未写完,下篇见
--------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2023.6.21
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。