客户端
客户端不需要显示的绑定端口号,而是由操作系统随机去绑定。TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept。TCP的客户端只需要向服务端发起链接请求
Client.hpp
#pragma once #include <iostream> #include <string> #include <cstring> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include "log.hpp" using namespace std; class Client { public: Client(const string &serverip, const uint16_t &port) : _serverip(serverip), _port(port), _sock(-1) { } void Init() { // 创建套接字 _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { LogMessage(FATAL, "create socket error"); exit(1); } // TCP的客户端也不需要显示绑定端口,让操作系统随机绑定 // TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept // TCP的客户端只需要向服务端发起链接请求 } void start() { // 向服务端发起链接请求 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = inet_addr(_serverip.c_str()); if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0) LogMessage(ERROR, "connect socket error"); // 和服务端通信 else { string line; while (1) { cout << "Please cin: " << endl; getline(cin, line); // 向服务端写 write(_sock, line.c_str(), line.size()); // 读服务端返回来的数据 char buff[1024]; int n = read(_sock, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = 0; cout << "接收到的消息为:" << buff << endl; } else break; } } } ~Client() { if(_sock >= 0) close(_sock); } private: int _sock; string _serverip; uint16_t _port; };
Client.cc
#include "Client.hpp" #include <memory> // 输出命令错误函数 void Usage(string proc) { cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; } int main(int argc, char* argv[]) { // 再运行客户端时,输入的指令需要包括主机ip和端口号 if(argc != 3) { Usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t port = atoi(argv[2]); unique_ptr<Client> client(new Client(serverip, port)); client->Init(); client->start(); return 0; }
服务端
那么对于服务端而言,必须要显式的去绑定端口号。则创建的套接字并不是负责通信的。创建好套接字和绑定完网络信息后,需要设置创建的套接字为监听状态。和UDP一样,服务端是不能指定IP的.
还需要注意的是:因为封装的线程池是单例模式,所以不需要创建对象,直接调用静态对象去调用类方法即可
步骤可分为:
- 创建监听套接字
- 绑定网络信息
- 设置套接字为监听状态
- 获取链接,得到通信的套接字
- 通信
- 关闭不需要的套接字
Server.hpp
#pragma once #include "Task.hpp" #include "ThreadPool.hpp" #include <sys/types.h> #include <sys/socket.h> #include <cstring> #include <netinet/in.h> #include <arpa/inet.h> class Server { public: Server(const uint16_t &port = 8000) : _port(port) { } void Init() { // 创建负责监听的套接字 面向字节流 _listenSock = socket(AF_INET, SOCK_STREAM, 0); if (_listenSock < 0) { LogMessage(FATAL, "create socket error!"); exit(1); } LogMessage(NORMAL, "create socket %d success!", _listenSock); // 绑定网络信息 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; if (bind(_listenSock, (struct sockaddr *)&local, sizeof(local)) < 0) { LogMessage(FATAL, "bind socket error!"); exit(3); } LogMessage(NORMAL, "bind socket success!"); // 设置socket为监听状态 if (listen(_listenSock, 5) < 0) { LogMessage(FATAL, "listen socket error!"); exit(4); } LogMessage(NORMAL, "listen socket success!"); } void start() { while (1) { // 因为线程池时单例模式,所以直接调用初始化 ThreadPool<Task>::getInstance()->run(); LogMessage(NORMAL, "Thread init success"); // server获取建立新连接 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); // 创建通信的套接字 // accept的返回值才是真正用于通信的套接字 _sock = accept(_listenSock, (struct sockaddr *)&peer, &len); if (_sock < 0) { // 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已 LogMessage(ERROR, "accept socket error, next"); continue; } LogMessage(NORMAL, "accept socket %d success", _sock); cout << "sock: " << _sock << endl; // 往线程池的任务队列里插入任务 ThreadPool<Task>::getInstance()->push(Task(_sock, ServerIO)); } } private: int _listenSock; // 负责监听的套接字 int _sock; // 通信的套接字 uint16_t _port; // 端口号 };
Server.cc
#include "Server.hpp" #include "daemon.hpp" #include <memory> // 输出命令错误函数 void Usage(string proc) { cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; } int main(int argc, char* argv[]) { // 启动服务端不需要指定IP if(argc != 2) { Usage(argv[0]); exit(1); } uint16_t port = atoi(argv[1]); unique_ptr<Server> server(new Server(port)); server->Init(); server->start(); return 0; }
实现效果
可以看到多个客户端同时访问也没有问题,并且所对应的套接字也就是文件描述符也不一样。
守护进程
守护进程是一种特殊的孤儿进程,其运行于后台,生存期较长并且独立与终端周期性的执行任务或者等待处理任务
进程分为前台运行和后台运行,每一个进程都会属于一个会话组里。每一个会话组都有且只有能一个前台进程。像上述的服务端,当运行服务端时,操作系统会将其分到含有bash的会话组内,并且将服务端置为前台任务进程,因此服务端运行时bash把放置后台这也就是为什么用户不能再bash继续输入命令的原因。
每一个会话组都会有一个组长,一般而言在bash中输入命令执行的进程都会分到bash的会话组内,这个会话组的组长即为bash。可以通过查看进程的SID确认进程的会话组
可以看到上述图片中运行了三个进程并置于后台,他们的SID也就是会话组都是一样的。那么如果将他们置于前台运行会发生什么呢
可以看到,置于前台运行后,命令行输入什么都没有反应了。也就是说,此时的bash被自动的放到了后台运行,证实了一个会话组只能有一个前台进程
输入ctr + Z 之后前台的进程就会把切回后台,但是切回后台后进程是阻塞状态的,因此输入bg + 作业号就可让进程启动。
服务端守护进程化
那么很显然,在业务逻辑上服务端肯定是需要守护进程化的。因为服务端没有特殊情况是不会关闭的,需要一直运行。如果服务端是前台进程的话,那服务端运行时bash都不能用了,显然不符合。
这里要介绍一个接口:
#include <unistd.h> pid_t setsid(void);
这个接口的作用是使调用的进程独立成为一个会话组并且为该组的组长。但是调用这个接口是有前置条件的:调用这个接口的进程不能为某个会话组的组长
守护进程化的步骤:
- 让调用进程忽略掉异常信号,因为其不受终端控制的
- 让调用进程不为组长
- 关闭或者重定向之前默认打开的文件,如0 1 2文件描述符
#pragma once #include <unistd.h> #include <signal.h> #include <cstdlib> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define DEV "/dev/null" void daemonSelf(const char *currPath = nullptr) { // 1. 让调用进程忽略掉异常的信号 signal(SIGPIPE, SIG_IGN); // 2. 让自己不是组长,setsid if (fork() > 0) exit(0); // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种! pid_t n = setsid(); assert(n != -1); // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件 int fd = open(DEV, O_RDWR); if(fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } else { close(0); close(1); close(2); } }
接着只需要服务端在初始化完成后调用这个函数,将自己设为守护进程化即可
一起来看看效果:
可以看到服务端启动后并不会影响bash,仍然可以在bash上输入指令去执行。客户端也能够很好的接收到数据,这就符合现实中服务端的逻辑。