1.Linux远程控制的网络程序
1.1.Linux远程控制的网络程序(普通版)
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建ThreadPool.hpp文件,写入下图五所示的代码,创建Task.hpp文件,写入下图六所示的代码,创建Lock.hpp文件,写入下图七所示的代码,创建Makefile文件,写入下图八所示的代码,使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,如下图九所示。
serverTcp.cc文件:
#include "util.hpp" #include "Task.hpp" #include "ThreadPool.hpp" #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> class ServerTcp; // 申明一下ServerTcp void execCommand(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char command[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, command, sizeof(command) - 1); //我们认为我们读到的都是字符串 if (s > 0) { command[s] = '\0'; logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command); // 考虑安全 std::string safe = command; if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink"))) { break; } FILE *fp = popen(command, "r"); if(fp == nullptr) { logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno)); break; } char line[1024]; while(fgets(line, sizeof(line)-1, fp) != nullptr) { write(sock, line, strlen(line)); } pclose(fp); logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } // 只要走到这里,一定是client退出了,服务到此结束 close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏! logMessage(DEBUG, "server close %d done", sock); } class ThreadData { public: uint16_t clientPort_; std::string clinetIp_; int sock_; ServerTcp *this_; public: ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts) : clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts) { } }; class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1), tp_(nullptr) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. bind绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 本地socket信息,写入sock_对应的内核区域 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3. 监听socket,为何要监听呢?tcp是面向连接的! if (listen(listenSock_, 5 /*后面再说*/) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 运行别人来连接你了 // 4. 加载线程池 tp_ = ThreadPool<Task>::getInstance(); } void loop() { tp_->start(); logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum()); while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4. 获取连接, accept 的返回值是一个新的socket fd ?? // 4.1 listenSock_: 监听 && 获取新的链接-> sock // 4.2 serviceSock: 给用户提供新的socket服务 int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5 提供服务, echo -> 小写 -> 大写 // 5.3 v3.2 Task t(serviceSock, peerIp, peerPort, execCommand); tp_->push(t); } } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; // 引入线程池 ThreadPool<Task> *tp_; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if (argc == 3) ip = argv[2]; ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; }
clientTcp.cc文件:
#include "util.hpp" // 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!! // 3. 需要listen吗?不需要的! // 4. 需要accept吗?不需要的! volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1. 创建socket SOCK_STREAM int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,connect 会自动帮我们进行bind! if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << sock << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); // 结尾不会有\n if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(sock); return 0; }
log.hpp文件:
#pragma once #include <cstdio> #include <ctime> #include <cstdarg> #include <cassert> #include <cstring> #include <cerrno> #include <stdlib.h> #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr:stdout; fprintf(out, "%s | %u | %s | %s\n", \ log_level[level], \ (unsigned int)time(nullptr),\ name == nullptr ? "unknow":name,\ logInfo); }
util.hpp文件:
#pragma once #include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <cassert> #include <ctype.h> #include <unistd.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "log.hpp" #define SOCKET_ERR 1 #define BIND_ERR 2 #define LISTEN_ERR 3 #define USAGE_ERR 4 #define CONN_ERR 5 #define BUFFER_SIZE 1024
ThreadPool.hpp文件:
#pragma once #include <iostream> #include <cassert> #include <queue> #include <memory> #include <cstdlib> #include <pthread.h> #include <unistd.h> #include <sys/prctl.h> #include "Lock.hpp" using namespace std; int gThreadNum = 15; template <class T> class ThreadPool { private: ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false) { assert(threadNum_ > 0); pthread_mutex_init(&mutex_, nullptr); pthread_cond_init(&cond_, nullptr); } ThreadPool(const ThreadPool<T> &) = delete; void operator=(const ThreadPool<T>&) = delete; public: static ThreadPool<T> *getInstance() { static Mutex mutex; if (nullptr == instance) //仅仅是过滤重复的判断 { LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁 if (nullptr == instance) { instance = new ThreadPool<T>(); } } return instance; } //类内成员, 成员函数,都有默认参数this static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); while (1) { tp->lockQueue(); while (!tp->haveTask()) { tp->waitForTask(); } //这个任务就被拿到了线程的上下文中 T t = tp->pop(); tp->unlockQueue(); t(); // 让指定的先处理这个任务 } } void start() { assert(!isStart_); for (int i = 0; i < threadNum_; i++) { pthread_t temp; pthread_create(&temp, nullptr, threadRoutine, this); } isStart_ = true; } void push(const T &in) { lockQueue(); taskQueue_.push(in); choiceThreadForHandler(); unlockQueue(); } ~ThreadPool() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } int threadNum() { return threadNum_; } private: void lockQueue() { pthread_mutex_lock(&mutex_); } void unlockQueue() { pthread_mutex_unlock(&mutex_); } bool haveTask() { return !taskQueue_.empty(); } void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } void choiceThreadForHandler() { pthread_cond_signal(&cond_); } T pop() { T temp = taskQueue_.front(); taskQueue_.pop(); return temp; } private: bool isStart_; int threadNum_; queue<T> taskQueue_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool<T> *instance; // const static int a = 100; }; template <class T> ThreadPool<T> *ThreadPool<T>::instance = nullptr;
Task.hpp文件:
#pragma once #include <iostream> #include <string> #include <functional> #include <pthread.h> #include "log.hpp" class Task { public: //等价于 // typedef std::function<void (int, std::string, uint16_t)> callback_t; using callback_t = std::function<void (int, std::string, uint16_t)>; private: int sock_; // 给用户提供IO服务的sock uint16_t port_; // client port std::string ip_; // client ip callback_t func_; // 回调方法 public: Task():sock_(-1), port_(-1) {} Task(int sock, std::string ip, uint16_t port, callback_t func) : sock_(sock), ip_(ip), port_(port), func_(func) {} void operator () () { logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\ pthread_self(), ip_.c_str(), port_); func_(sock_, ip_, port_); logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\ pthread_self(), ip_.c_str(), port_); } ~Task() {} };
Lock.hpp文件:
#pragma once #include <iostream> #include <pthread.h> class Mutex { public: Mutex() { pthread_mutex_init(&lock_, nullptr); } void lock() { pthread_mutex_lock(&lock_); } void unlock() { pthread_mutex_unlock(&lock_); } ~Mutex() { pthread_mutex_destroy(&lock_); } private: pthread_mutex_t lock_; }; class LockGuard { public: LockGuard(Mutex *mutex) : mutex_(mutex) { mutex_->lock(); std::cout << "加锁成功..." << std::endl; } ~LockGuard() { mutex_->unlock(); std::cout << "解锁成功...." << std::endl; } private: Mutex *mutex_; };
Makefile文件:
.PHONY:all all:clientTcp serverTcp clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcp:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcp clientTcp
注:
1.popen函数的功能是可以分三步描述:首先创建一个管道,然后自动创建子进程执行command命令,将子进程执行后的数据通过管道放入到popen函数返回的FILE*文件中。popen函数函数声明如下图所示。参数command是需要被执行的命令,参数type指定文件指针的打开模式,可以是r(只读)或w(只写)等。
如果文件打开失败返回null并设置错误码,如果文件打开成功返回文件指针。
使用popen函数需要包含<stdio.h>头文件。
popen函数返回的文件指针使用完后,应该使用pclose函数将文件关闭。
1.2.Linux远程控制的网络程序(守护进程版)
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建ThreadPool.hpp文件,写入下图五所示的代码,创建Task.hpp文件,写入下图六所示的代码,创建Lock.hpp文件,写入下图七所示的代码,创建daemonize.hpp文件,写入下图八所示的代码,创建Makefile文件,写入下图九所示的代码,使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,如下图十所示。
这里只要使用./udpServer 8081命令将服务端程序运行起来,就已经将服务端程序部署在了服务器中,即使此时关闭xshell选项卡退出用户登录,其他客户端主机也可以连接到云服务器上的服务端程序。
注:
1.一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则一直运行。
守护进程有一个特点,那就是其ppid为1,也就是说其父进程必须是操作系统,如下图所示,其中PGID为当前进程所属的进程组,SID为当前进程的会话ID。
进程组解释:
我们使用sleep 1000 | sleep 2000 |sleep 3000 &命令在后台创建三个进程,然后使用 ps axj | head -1 && ps axj | grep sleep命令查看这三个进程,如下图所示。三个进程的ppid都是28406,即bash进程,三个进程同属于28547进程组,即第一个进程的pid,也就是说同时创建三个进程,第一个创建的进程是该进程组的组长。
因为三个进程同属一个进程组,所以我们使用jobs命令可以看到这三个进程是在一个任务组的,如下图所示。
会话解释:
我们使用主机登录云服务器用户时,Linux云服务器会给我们形成一个叫会话的东西,会话内部由多个进程组构成,其中必须有且仅有一个前台进程组,如下图一所示。
操作系统提供的注销就是退出并重新登陆用户,重新创建会话及内部的进程组。我们有时电脑卡可以用注销来解决,本质就是将此刻会话里面的进程组全部关闭。
上图所示三个sleep进程的会话ID都是28375,使用 ps axj | head -1 && ps ajx | grep 28375 命令,如下图二所示,bash进程的pid为28375,同时也是进程组的组长和会话话首进程。创建三个sleep进程时,三个sleep进程构建了自己的进程组,但是三个sleep进程所属的会话依旧是bash会话。
当我们登录云服务器用户时,bash构建了一个会话,自己是会话话首,如果我们后面再启动新进程或启动新进程组,它们都是属于该bash会话的。
如果我们使用fg命令把某个进程任务启动到前台,那么就不能使用类似ls等命令了,如下图三所示,因为一个会话有且仅有一个前台进程组,如果将某个进程任务启动到前台,那么bash命令行解释器就会放到后台,放到后台就无法接收输入的命令了。
因此,在命令行中启动一个进程,本质是在会话中启动一个进程组(组内可以是一个进程),来完成某种任务。所有会话内的进程fork创建子进程或线程,一般而言依旧属于当前会话。
守护进程:对外提供服务的服务器进程,不能属于某个会话,否则服务器进程会受到会话对应的用户登录或注销操作的影响。我们应该将服务器进程脱离某一个用户的会话,让其独立的在服务器中形成自己的新会话,即自成进程组、自成新会话,这种进程就是守护进程或精灵进程。
2.要编写守护进程,需要使用setsid函数,将调用的进程设置为独立的会话,setsid函数声明如下图所示,哪一个进程调用该函数,那么该进程就会自成进程组、自成新会话。
如果调用成功会返回调用该函数进程的pid,如果调用失败返回-1,并且设置错误码。
使用setsid函数需要包含<unistd.h>头文件。
进程组的组长不能调用setsid函数,如果强行调用那么就会调用失败。但是只要正常启动某个可执行程序,该可执行程序一定就是组长,如何使得服务器进程不成为组长呢?答案是可以让服务器进程成为进程组内的第二个进程,常规做法:fork子进程,子进程就不再是组长进程了,它就可以成功调用setsid函数。
编写守护进程必做的工作:创建子进程,执行setsid函数和服务器进程的服务操作。
编写守护进程选做的工作:
(1)对于管道,写端一直在写,读端关闭,写端会被SIGPIPE信号终止。如果客户端退出,服务端也会收到SIGPIPE信号,因此在服务端可以忽略SIGPIPE信号。
(2)更改守护进程的工作目录,便于以绝对路径的方式,寻找Linux文件结构中的任何一个配置文件。
(3)一旦服务器进程成为守护进程,那么该进程就和键盘显示器等没有关系了,即和标准输入、标准输出、标准错误没有关系了,因此有两种做法。
第一种(不推荐):将0、1、2文件描述符关闭,但很少有人这样做。
第二种(推荐):/dev/null 是Linux下的垃圾桶或信息黑洞,如下图所示,凡是将消息写入到该目录操作或从该目录读取消息操作都会被直接丢弃。因此这里可以打开/dev/null,并对0、1、2文件描述符重定向。
3.想要关闭部署在云服务器上的服务进程,可以使用ps axj | grep serverTcp命令找到对应服务进程的pid,然后kill -9 pid值杀死服务进程。
4.守护进程的进程名一般以d结尾,因此这里将服务进程名改为serverTcpd,如下图所示。
5. 这里因为daemonize.hpp文件设置守护进程时,将0、1、2文件描述符重定向到了/dev/null垃圾桶,所以所有的日志信息都不会再被打印了。我们可以在log.txt文件中 #define LOGFILE "serverTcp.log",然后打开LOGFILE文件,使用dup2函数将标准输出和标准错误都写入到LOGFILE文件中。