目录
1.Linux远程控制的网络程序
1.1.Linux远程控制的网络程序(普通版)
1.2.Linux远程控制的网络程序(守护进程版)
2.TCP协议通信流程
2.1.通信流程总览
2.2.三次握手的过程
2.3.数据传输的过程
2.4.四次挥手的过程
3.TCP和UDP对比
创建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
#include #include #include 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 ::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 *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
#include #include #include #include #include #include #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
#include #include #include #include #include #include #include #include #include #include #include #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
#include #include #include #include #include #include #include #include "Lock.hpp" using namespace std; int gThreadNum = 15; template 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 &) = delete; void operator=(const ThreadPool &) = delete; public: static ThreadPool *getInstance() { static Mutex mutex; if (nullptr == instance) //仅仅是过滤重复的判断 { LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁 if (nullptr == instance) { instance = new ThreadPool (); } } return instance; } //类内成员, 成员函数,都有默认参数this static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = static_cast *>(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 taskQueue_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool *instance; // const static int a = 100; }; template ThreadPool *ThreadPool ::instance = nullptr; Task.hpp文件:
#pragma once #include
#include #include #include #include "log.hpp" class Task { public: //等价于 // typedef std::function callback_t; using callback_t = std::function ; 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
#include 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函数需要包含
头文件。 popen函数返回的文件指针使用完后,应该使用pclose函数将文件关闭。
创建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选项卡退出用户登录,其他客户端主机也可以连接到云服务器上的服务端程序。
serverTcp.cc文件:
#include "util.hpp" #include "Task.hpp" #include "ThreadPool.hpp" #include "daemonize.hpp" #include
#include #include #include class ServerTcp; // 申明一下ServerTcp // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if (strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 可以进行大小写转化了 for (int i = 0; i < s; i++) { if (isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } 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); } 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; } // 我们是以r方式打开的文件,没有写入 // 所以我们无法通过dup的方式得到对应的结果 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) { quit_ = false; } ~ServerTcp() { if (listenSock_ >= 0) close(listenSock_); } 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 ::getInstance(); } void loop() { // signal(SIGCHLD, SIG_IGN); // only Linux tp_->start(); logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum()); while (!quit_) { 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(quit_) break; 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.3 Task t(serviceSock, peerIp, peerPort, execCommand); tp_->push(t); } } bool quitServer() { quit_ = true; } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; // 引入线程池 ThreadPool *tp_; // 安全退出 bool quit_; }; 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 *svrp = nullptr; void sigHandler(int signo) { if(signo == 3 && svrp != nullptr) svrp->quitServer(); logMessage(DEBUG, "server quit save!"); } // ./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]; daemonize(); // 我们的进程就会成为守护进程 signal(3, sigHandler); Log log; log.enable(); ServerTcp svr(port, ip); svr.init(); svrp = &svr; 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
#include #include #include #include #include #include #include #include #include #include #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"}; #define LOGFILE "serverTcp.log" class Log { public: Log():logFd(-1) {} void enable() { umask(0); logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666); assert(logFd != -1); dup2(logFd, 1); dup2(logFd, 2); } ~Log() { if(logFd != -1) { fsync(logFd); close(logFd); } } private: int logFd; }; // 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); fflush(out); // 将C缓冲区中的数据刷新到OS fsync(fileno(out)); // 将OS中的数据尽快刷盘 } util.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include #include #include #include #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
#include #include #include #include #include #include #include #include "Lock.hpp" using namespace std; int gThreadNum = 15; template 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 &) = delete; void operator=(const ThreadPool &) = delete; public: static ThreadPool *getInstance() { static Mutex mutex; if (nullptr == instance) //仅仅是过滤重复的判断 { LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁 if (nullptr == instance) { instance = new ThreadPool (); } } return instance; } //类内成员, 成员函数,都有默认参数this static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = static_cast *>(args); // prctl(PR_SET_NAME, "follower"); // 更改线程名称 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 taskQueue_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool *instance; // const static int a = 100; }; template ThreadPool *ThreadPool ::instance = nullptr; Task.hpp文件:
#pragma once #include
#include #include #include #include "log.hpp" class Task { public: //等价于 // typedef std::function callback_t; using callback_t = std::function ; 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
#include 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_; }; daemonize.hpp文件:
#pragma once #include
#include #include #include #include #include #include void daemonize() { int fd = 0; // 1. 忽略SIGPIPE signal(SIGPIPE, SIG_IGN); // 2. 更改进程的工作目录 // chdir(); // 3. 让自己不要成为进程组组长 if (fork() > 0) exit(0); // 4. 设置自己是一个独立的会话 setsid(); // 5. 重定向0,1,2 if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3 { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); // 6. 关闭掉不需要的fd if(fd > STDERR_FILENO) close(fd); } // 6. close(0,1,2)// 严重不推荐 } Makefile文件:
.PHONY:all all:clientTcp serverTcpd clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcpd:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcpd clientTcp
注:
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函数需要包含
头文件。 进程组的组长不能调用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文件中。
如果在log.hpp文件中进行打印日志的输出重定向,向日志文件LOGFILE打印,那么每次打印日志都要打开日志文件LOGFILE,效率很低。
我们在log.hpp文件中定义一个Log类,Log类中定义enable函数打开日志文件LOGFILE,并将标准输出和标准错误dup2重定向到glogFd日志文件中。在服务端主函数中定义一个Log类对象log,调用log对象的enable函数打开日志文件LOGFILE并进行重定向工作,这样每次打印日志时就不用重复打开日志文件LOGFILE和dup2重定向了。
6.想要将一个进程变为守护进程,除了这里我们使用的方案(我们这里使用的方案是主流,可以自定义一些操作),还有两种简单的方案:
方案一:进程中调用daemon函数,该进程就会变成守护进程。
daemon函数⽤于创建守护进程,使进程脱离控制台,在后台运行。
参数:
nochdir:更改进程的工作目录。参数nochdir为0时,即可将工作目录修改为根目录(“/”)。
noclose:输出重定向标准输入、输出和错误。参数noclose为0时,输入、输出以及错误输出重定向到/dev/null。
返回值:
deamon()调⽤了fork(),如果fork成功,那么⽗进程就调⽤_exit(2)退出,所以看到的错误信息全部是⼦进程产⽣的。如果成功函数返回0,否则返回-1并设置errno。
使用daemon函数需要包含
头文件。 方案二:进程在运行时使用nohup修饰,例如 nohup ./serverTcpd 运行服务程序。
nohup是将对应运行的程序自成进程组(不会自成会话,会话仍属于对应bash会话),且不受用户登陆注销影响。因为nohup对应进程不会自成会话,所以nohup的进程并不是守护进程,但与守护进程功能相同,nohup对应进程可以看作是后台进程。nohup对应进程后还会生成nohup.out文件保存对应进程输出内容(日志信息)。
7.服务器进程是一个守护进程,服务器进程要退出就需要发送退出信号,我们这里在服务端ServerTcp中新增一个quit_成员变量作为退出标志位,ServerTcp构造函数中默认初始化为false,在ServerTcp中signal捕捉3号信号执行sigHandler函数,在sigHandler函数中调用quitServer函数将quit_置为true,在loop函数中如果quit_被设置为true则退出循环停止服务。
下图是基于TCP协议的客户端/服务器程序的一般流程:
下面我们结合TCP协议的通信流程,来初步认识一下三次握手和四次挥手,以及建立连接和断开连接与各个网络接口之间的对应关系。
下面在学习socket API时要注意应用程序和TCP协议是如何交互的:
· 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect会发出SYN段。
· 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read返回0就表明收到了FIN段。
初始化服务器
当服务器完成套接字创建、绑定以及监听的初始化动作之后,就可以调用accept函数阻塞等待客户端发起请求连接了。
服务器初始化:
· 调用socket,创建文件描述符。
· 调用bind,将当前的文件描述符和IP/PORT绑定在一起,如果这个端口已经被其他进程占用了,就会bind失败。
· 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备。
· 调用accept,并阻塞,等待客户端连接到来。
建立连接而客户端在完成套接字创建后,就会在合适的时候通过connect函数向服务器发起连接请求,而客户端在connect的时候本质是通过某种方式向服务器三次握手,因此connect的作用实际就是触发三次握手。
建立连接的过程:
· 调用socket,创建文件描述符。
· 调用connect,向服务器发起连接请求。
· connect会发出SYN段并阻塞等待服务器应答(第一次)。
· 服务器收到客户端的SYN,会应答一个SYN-ACK段表示“同意建立连接”(第二次)。
· 客户端收到SYN-ACK后会从connect返回,同时应答一个ACK段(第三次)。
这个建立连接的过程,通常称为三次握手。需要注意的是,连接并不是立马建立成功的,由于TCP属于传输层协议,因此在建立连接时双方的操作系统会自主进行三次协商,最后连接才会建立成功。
数据交互
连接一旦建立成功并且被accept获取上来后,此时客户端和服务器就可以进行数据交互了。需要注意的是,连接建立和连接被拿到用户层是两码事,accept函数实际不参与三次握手这个过程,因为三次握手本身就是底层TCP所做的工作。accept要做的只是将底层已经建立好的连接拿到用户层,如果底层没有建立好的连接,那么accept函数就会阻塞住直到有建立好的连接。而双方在进行数据交互时使用的实际就是read和write,其中write就叫做写数据,read就叫做读数据。write的任务就是把用户数据拷贝到操作系统,而拷贝过去的数据何时发以及发多少,就是由TCP决定的。而read的任务就是把数据从内核读到用户。
数据传输的过程:
· 建立连接后,TCP协议提供全双工的通信服务,所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据,相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据。
· 服务器从accept返回后立刻调用read,读socket就像读管道一样,如果没有数据到达就阻塞等待。
· 这时客户端调用write发送请求给服务器,服务器收到后从read返回,对客户端的请求进行处理,在此期间客户端调用read阻塞等待服务器端应答。
· 服务器调用write将处理的结果发回给客户端,再次调用read阻塞等待下一条请求。
客户端收到后从read返回,发送下一条请求,如此循环下去。
断开连接
当双方通信结束之后,需要通过四次挥手的方案使双方断开连接,当客户端调用close关闭连接后,服务器最终也会关闭对应的连接。而其中一次close就对应两次挥手,因此一对close最终对应的就是四次挥手。
断开连接的过程:
· 如果客户端没有更多的请求了,就调用close关闭连接,客户端会向服务器发送FIN段(第一次)。
· 此时服务器收到FIN后,会回应一个ACK,同时read会返回0(第二次)。
· read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次)。
· 客户端收到FIN,再返回一个ACK给服务器(第四次)。
这个断开连接的过程,通常称为四次挥手。
问题:为什么要断开连接?
答:建立连接本质上是为了保证通信双方都有专属的连接,这样我们就可以加入很多的传输策略,从而保证数据传输的可靠性。但如果双方通信结束后不断开对应的连接,那么系统的资源就会越来越少。
因为服务器是会收到大量连接的,操作系统必须要对这些连接进行管理,在管理连接时我们需要“先描述再组织”。因此当一个连接建立后,在服务端就会为该连接维护对应的数据结构,并且会将这些连接的数据结构组织起来,此时操作系统对连接的管理就变成了对链表的增删查改。
如果一个连接建立后不断开,那么操作系统就需要一直为其维护对应的数据结构,而维护这个数据结构是需要花费时间和空间的,因此当双方通信结束后就应该将这个连接断开,避免系统资源的浪费,这其实就是TCP比UDP更复杂的原因之一,因为TCP需要对连接进行管理。
· 可靠传输 vs 不可靠传输
· 有连接 vs 无连接
· 字节流 vs 数据报