目录

1. TCP网络编程

1.1 前期代码

log.hpp

tcp_server.cc

1.2accept和单进程版代码

1.3 多进程版strat代码

1.4client.cc客户端

1.5多进程版strat代码改进+多线程

1.6 线程池版本

Task.hpp

lockGuard.hpp

thread.hpp

threadPool.hpp

多个回调任务

tcp_client.cc

tcp_server.hpp

2. 笔试选择题

答案及解析

本篇完。


1. TCP网络编程

框架和前面udp通信一样,接口函数上一篇也讲了,这里直接放一部分代码:

1.1 前期代码

log.hpp

#pragma once#include #include #include #include #include // 日志是有日志级别的#define DEBUG 0#define NORMAL1#define WARNING 2#define ERROR 3#define FATAL 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)void logMessage(int level, const char *format, ...)// 可变参数{#ifndef DEBUG_SHOWif(level== DEBUG) {return;}#endifchar stdBuffer[1024]; // 标准日志部分time_t timestamp = time(nullptr); // 获取时间戳// struct tm *localtime = localtime(&timestamp); // 转化麻烦就不写了snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; // 自定义日志部分va_list args; // 提取可变参数的 -> #include  了解一下就行va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args); // 相当于ap=nullptrprintf("%s%s\n", stdBuffer, logBuffer);// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);// fclose(fp);}

client.cc

#include int main(){return 0;}

tcp_server.hpp

#pragma once#include #include #include #include #include #include #include #include  // 网络四件套#include #include #include #include "log.hpp"class TcpServer{protected:const static int gbacklog = 20;// listen的第二个参数,现在先不管public:TcpServer(uint16_t port, std::string ip=""):_listensock(-1), _port(port), _ip(ip){}void initServer(){// 1. 创建socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif(_listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3// 2. bind -- 文件 + 网络struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说{logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}void start(){while(true){sleep(7);}}~TcpServer(){}protected:uint16_t _port;std::string _ip;int _listensock;};

tcp_server.cc

#include "tcp_server.hpp"#include static void usage(std::string proc){std::cout << "\nUsage: " << proc << " port\n" << std::endl;}// ./tcp_server portint main(int argc, char *argv[]){if(argc != 2){usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);std::unique_ptr svr(new TcpServer(port));svr->initServer();svr->start();return 0;}

编译运行:

此时程序就跑起来了。


1.2accept和单进程版代码

再写start函数:

void start(){while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程service(servicesock, client_ip, client_port);}}

第4步获取链接,写在start函数中,如上图所示,使用accept来接收客户端的连接请求,有点像udp中的recvfrom一样,只是accept是用来接收套接字的连接请求,而recvfrom是接收套接字中的数据的。manaccept:

accept系统调用的参数和recvfrom中的一样,如上图所示,accept的作用就是接收来自套接字中的连接请求,也就是来自客户端的连接请求。

设置为listen状态的套接字不用了通信,只是用来接收客户端的网络请求,具体体现在accept的返回值上。

第一步中创建的套接字就像是一个门童,使用accept来接收客户端的连接请求,如果有连接请求并且接收成功,那么会返回一个文件描述符fd。 这里的文件描述符sock和前面的_listensock不是一个东西,_listensock是我们创建的,是专门用来接收连接请求的,而accept返回的sock是操作系统在接收成功连接请求后新创建的套接字的文件描述符。 sock指向的文件描述符是服务端专门用来和客户端通信的,所以每有一个客户端向服务器发起连接请求,客户端接收成功够都会创建一个套接字用来一对一的提供服务。

如果accept接收连接请求失败,则返回-1,并且设置错误码。这里的失败并不是致命的,就像门童拉客一样,拉客失败也没有什么,继续进行下一次拉客就行。 所以accept失败也没有什么,继续接收下一个连接请求即可,所以在代码中,如果接收失败,使用了continue继续接收连接请求。

accept是阻塞执行的,在没有网络连接请求的时候,会阻塞等待,直到客户端的网络连接请求到来。


至此,进行tcp网络通信的所有准备工作已经做完,接下来就是进行具体的服务了,也就是读取客户端发送来的数据并做相应的处理了。看一下start在最后调用的service函数:

static void service(int sock, const std::string &clientip, const uint16_t &clientport){//echo serverchar buffer[1024];while(true){// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;}else if(s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}}

如代码所示,就是服务器指向的具体服务函数。 客户端读取客户端发送来的数据时,是从accept返回的文件描述符sock指向的套接字中读取数据的,因为这个套接字是专门用来服务客户端的。

读取数据时,使用的是read系统调用,和读取普通文件一模一样。

数据读取成功后,做一些处理,先将读取的数据打印一下,加一个回显,再给客户端发送过去。

发送数据时,使用的是write系统调用,写入的也是sock指向的套接字,同样与向普通文件中写入数据一模一样。

在读取普通文件的时候,如果文件被读完了,read会返回0,表示文件的内容被读取完毕。 但是在使用read读取tcp套接字的时候,如果读取到0,表示客户端关闭了它的套接字,代表着客户端不再进行网络通信了,此时服务端就可以结束这次通信了,也就是将sock指向的套接字关闭。

这里再放下tcp_server.hpp

#pragma once#include #include #include #include #include #include #include #include  // 网络四件套#include #include #include #include "log.hpp"static void service(int sock, const std::string &clientip, const uint16_t &clientport){//echo serverchar buffer[1024];while(true){// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;}else if(s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}}class TcpServer{protected:const static int gbacklog = 20;// listen的第二个参数,现在先不管public:TcpServer(uint16_t port, std::string ip=""):_listensock(-1), _port(port), _ip(ip){}void initServer(){// 1. 创建socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif(_listensock  要从单进程改成多线程service(servicesock, client_ip, client_port);}}~TcpServer(){}protected:uint16_t _port;std::string _ip;int listensock;};

编译运行:

telnet 是一个远程链接命令,这里切换到了root输入了这个下载指令:yum -y install telnet telnet-server xinetd,以后输入telnet 127.0.0.1 7070链接到启动了的程序,然后Ctrl+],再回车就能发消息了,最后Ctrl+]输入quit就退出了。


1.3 多进程版strat代码

只用改strat函数:

void start(){// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程// service(servicesock, client_ip, client_port);// 2 version 2.0 -- 多进程版 --- 创建子进程// 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能pid_t id = fork();assert(id != -1);if(id == 0) // 子进程{// 子进程会不会继承父进程打开的文件与文件fd呢? -> 会// 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要close(_listensock); // 关闭自己不需要的套接字service(servicesock, client_ip, client_port); // 子进程对新链接提供服务exit(0); // 僵尸状态}// 父进程close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少// 如果父进程关闭servicesock,会不会影响子进程" />

此时就完整了多进程的第一个版本。


1.4client.cc客户端

看看接口,manaccept

如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,

accept()返回时传出客户端的地址和端口号。

addr是一个传输出型参数,如果给addr 参数传NULL,表示不关心客户端的地址。

addrlen参数是一个输入输出型参数(value-result argument),

输入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,

manconnect

客户端需要调用connect()连接服务器; connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址; connect()成功返回0,出错返回-1。

client.cc

#include #include #include #include #include #include #include #include #include void usage(std::string proc){std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;}// ./tcp_client targetIp targetPortint main(int argc, char *argv[]){if (argc != 3){usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);int sock = 0;sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" < connectstruct 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());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) // 比UDP多了这一步{std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl; // 到这链接成功了while (true){std::string line;std::cout < 0){buffer[s] = 0;std::cout << "server 回显# " << buffer << std::endl;}else // 关闭链接或者读取失败{break;}}close(sock);return 0;}

编译运行:


1.5多进程版strat代码改进+多线程

直接放代码:

void start(){// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程// service(servicesock, client_ip, client_port);// // 2 version 2.0 -- 多进程版 --- 创建子进程// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能// pid_t id = fork();// assert(id != -1);// if(id == 0) // 子进程// {// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要// close(_listensock); // 关闭自己不需要的套接字// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务// exit(0); // 僵尸状态// }// // 父进程// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少// // 如果父进程关闭servicesock,会不会影响子进程" />

还是和1.4 一样的效果,创建进程的成本是很高的,所以再改成多线程版,直接放代码:

(给Makefile加上-lpthread)

这是改的部分:

tcp_server.hpp:

#pragma once#include #include #include #include #include #include #include #include  // 网络四件套#include #include #include #include #include #include "log.hpp"static void service(int sock, const std::string &clientip, const uint16_t &clientport){//echo serverchar buffer[1024];while(true){// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;}else if(s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}}class ThreadData{public:int _sock;std::string _ip;uint16_t _port;};class TcpServer{protected:const static int gbacklog = 20;// listen的第二个参数,现在先不管static void *threadRoutine(void *args) // 加上static就没this指针了{pthread_detach(pthread_self()); // 线程分离ThreadData *td = static_cast(args);service(td->_sock, td->_ip, td->_port);delete td;return nullptr;}public:TcpServer(uint16_t port, std::string ip=""):_listensock(-1), _port(port), _ip(ip){}void initServer(){// 1. 创建socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif(_listensock  用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程// service(servicesock, client_ip, client_port);// // 2 version 2.0 -- 多进程版 --- 创建子进程// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能// pid_t id = fork();// assert(id != -1);// if(id == 0) // 子进程// {// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要// close(_listensock); // 关闭自己不需要的套接字// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务// exit(0); // 僵尸状态// }// // 父进程// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少// // 如果父进程关闭servicesock,会不会影响子进程?// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号// // version2.1 -- 多进程版 -- version 2.0 改版// pid_t id = fork();// if(id == 0)// {// // 子进程// close(_listensock);// if(fork() > 0)// 再创建子进程,子进程本身// exit(0); //子进程本身立即退出// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程// service(servicesock, client_ip, client_port);// exit(0);// }// // 父进程// waitpid(id, nullptr, 0); // 不会阻塞// close(servicesock);// version 3 --- 多线程版本// 创建进程的成本是很高的,所以再改成多线程版(不封装了)ThreadData *td = new ThreadData();td->_sock = servicesock;td->_ip = client_ip;td->_port = client_port;pthread_t tid;// 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类}}~TcpServer(){}protected:uint16_t _port;std::string _ip;int _listensock;};

编译运行:还是和上面一样的效果


1.6 线程池版本

把以前写的线程池和有关的代码拷过来:(只有Task.hpp要改一改)

Task.hpp

#pragma once#include // typedef std::function func_t;using func_t = std::function; // 和上一行一样的效果class Task{public:Task(){}Task(int sock, const std::string ip, uint16_t port, func_t func): _sock(sock), _ip(ip), _port(port), _func(func){}void operator ()(const std::string &name){_func(_sock, _ip, _port, name);}public:int _sock;std::string _ip;uint16_t _port;// int type;func_t _func;};

lockGuard.hpp

#pragma once#include #include class Mutex{public:Mutex(pthread_mutex_t* mtx) :_pmtx(mtx){}void lock(){pthread_mutex_lock(_pmtx);// std::cout << "进行加锁成功" << std::endl;}void unlock(){pthread_mutex_unlock(_pmtx);// std::cout << "进行解锁成功" << std::endl;}~Mutex(){}protected:pthread_mutex_t* _pmtx;};class lockGuard // RAII风格的加锁方式{public:lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化:_mtx(mtx){_mtx.lock();}~lockGuard(){_mtx.unlock();}protected:Mutex _mtx;};

thread.hpp

#pragma once#include #include #include // typedef std::function fun_t;typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_tclass ThreadData // 线程数据{public:void *_args; // 真实参数std::string _name; // 名字};class Thread // 封装的线程{public:Thread(int num, fun_t callback, void *args) : _func(callback) // 回调函数{char nameBuffer[64];snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer_name = nameBuffer;_tdata._args = args; // 线程构造时把参数和名字带给线程数据_tdata._name = _name;}void start() // 启动线程{pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据}void join() // join自己{pthread_join(_tid, nullptr);}std::string name() // 返回线程名{return _name;}~Thread() // 析构什么也不做{}protected:std::string _name; // 线程名字pthread_t _tid; // 线程tidfun_t _func; // 线程要执行的函数ThreadData _tdata; // 线程数据};

threadPool.hpp

#include #include #include #include #include #include "thread.hpp"#include "lockGuard.hpp"const int g_thread_num = 7;// 线程池->有一批线程,一批任务,有任务push有任务pop,本质是: 生产消费模型templateclass ThreadPool{private:ThreadPool(int thread_num = g_thread_num):_num(thread_num){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);for(int i = 1; i <= _num; ++i){_threads.push_back(new Thread(i, routine, this));}}ThreadPool(const ThreadPool &other) = delete;const ThreadPool &operator=(const ThreadPool &other) = delete;public:static ThreadPool *getThreadPool(int num = g_thread_num) // 多线程使用单例的过程{// 可以有效减少未来必定要进行加锁检测的问题// 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为// 如果这里不加if,未来任何一个线程想获取单例,都必须调用getThreadPool接口// 一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的if (nullptr == thread_ptr) {lockGuard lockguard(&mutex);// pthread_mutex_lock(&mutex);if (nullptr == thread_ptr){thread_ptr = new ThreadPool(num);}// pthread_mutex_unlock(&mutex);}return thread_ptr;}void run() // 1. 线程池的整体启动{for (auto &iter : _threads){iter->start();std::cout <name() << " 启动成功" < 两个参数 -> 类型不匹配 -> 所以加static// 消费过程 -> 访问_task_queue -> 静态访问不了 -> 构造函数传this指针ThreadData *td = (ThreadData *)args;ThreadPool *tp = (ThreadPool *)td->_args;while (true){T task;{lockGuard lockguard(tp->getMutex()); // 出花括号自动调用析构,花括号里的接口全是加锁的while (tp->isEmpty()) // 空就等待{tp->waitCond();}// 任务队列不为空,读取任务task = tp->getTask(); // 是共享的-> 将任务从共享,拿到自己的私有空间}task(td->_name); // 告诉哪一个线程去处理这个任务就行了}}void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务{lockGuard lockguard(&lock); // 加锁,执行完这个函数自动解锁_task_queue.push(task); // 生产一个任务pthread_cond_signal(&cond); // 唤醒一个线程}// void joins()// {// for (auto &iter : _threads)// {// iter->join();// }// }~ThreadPool(){for (auto &iter : _threads){// iter->join();delete iter;}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}protected:std::vector _threads; // 保存一堆线程的容器int _num; // 线程的数量std::queue _task_queue; // 任务队列pthread_mutex_t lock;pthread_cond_t cond;static ThreadPool *thread_ptr; // 懒汉模式的单例对象指针static pthread_mutex_t mutex; // 单例对象的锁};template ThreadPool *ThreadPool::thread_ptr = nullptr; // 定义初始化为空template pthread_mutex_t ThreadPool::mutex = PTHREAD_MUTEX_INITIALIZER; // 定义锁

多个回调任务

改下tcp_server.hpp:

tcp_server.hpp

#pragma once#include #include #include #include #include #include #include #include  // 网络四件套#include #include #include #include #include #include #include "log.hpp"#include "threadPool.hpp"#include "Task.hpp"// static void service(int sock, const std::string &clientip, const uint16_t &clientport)// {// //echo server// char buffer[1024];// while(true)// {// // read && write 可以直接被使用// ssize_t s = read(sock, buffer, sizeof(buffer)-1);// if(s > 0)// {// buffer[s] = 0; // 将发过来的数据当做字符串// std::cout << clientip << ":" << clientport << "# " << buffer < 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);break;}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}close(sock);}// class ThreadData// {// public:// int _sock;// std::string _ip;// uint16_t _port;// };class TcpServer{protected:const static int gbacklog = 20;// listen的第二个参数,现在先不管// static void *threadRoutine(void *args) // 加上static就没this指针了// {// pthread_detach(pthread_self()); // 线程分离// ThreadData *td = static_cast(args);// service(td->_sock, td->_ip, td->_port);// delete td;// return nullptr;// }public:TcpServer(uint16_t port, std::string ip=""):_listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool::getThreadPool()){}void initServer(){// 1. 创建socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif(_listensock  用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号// signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态_threadpool_ptr->run();while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程// service(servicesock, client_ip, client_port);// // 2 version 2.0 -- 多进程版 --- 创建子进程// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能// pid_t id = fork();// assert(id != -1);// if(id == 0) // 子进程// {// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要// close(_listensock); // 关闭自己不需要的套接字// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务// exit(0); // 僵尸状态// }// // 父进程// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少// // 如果父进程关闭servicesock,会不会影响子进程?// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号// // version2.1 -- 多进程版 -- version 2.0 改版// pid_t id = fork();// if(id == 0)// {// // 子进程// close(_listensock);// if(fork() > 0)// 再创建子进程,子进程本身// exit(0); //子进程本身立即退出// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程// service(servicesock, client_ip, client_port);// exit(0);// }// // 父进程// waitpid(id, nullptr, 0); // 不会阻塞// close(servicesock);// // version 3 --- 多线程版本// // 创建进程的成本是很高的,所以再改成多线程版(不封装了)// ThreadData *td = new ThreadData();// td->_sock = servicesock;// td->_ip = client_ip;// td->_port = client_port;// pthread_t tid;// // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的// pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类// verison4 --- 线程池版本Task t(servicesock, client_ip, client_port, service);_threadpool_ptr->pushTask(t);}}~TcpServer(){}protected:uint16_t _port;std::string _ip;int _listensock;std::unique_ptr<ThreadPool> _threadpool_ptr;};

编译运行

完成了多个线程实现回显任务的情景。下面写一个回调的函数,实现小写字母转大写字母:

static void change(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name){//一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接char buffer[1024];// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;std::string message;char *start = buffer;while(*start){char c;if(islower(*start)) {c = toupper(*start);}else{c = *start;}message.push_back(c);++start;}write(sock, message.c_str(), message.size());}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));}close(sock);}

只需要改一下回调方法:

把client.cc改成循环的:

tcp_client.cc

#include #include #include #include #include #include #include #include #include void usage(std::string proc){std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;}// ./tcp_client targetIp targetPortint main(int argc, char *argv[]){if (argc != 3){usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);bool alive = false;int sock = 0;std::string line;while (true) // 客户端不断的链接{if (!alive){sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" < connectstruct 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());if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) // 比UDP多了这一步{std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl; // 到这链接成功了alive = true;}std::cout < 0) // ssize_t有符号的整数{char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样if (s > 0){buffer[s] = 0;std::cout << "server 回显# " << buffer << std::endl;}else if (s == 0){alive = false;close(sock);}}else // 关闭链接或者读取失败{alive = false;close(sock);}}return 0;}/*int main(int argc, char *argv[]){if (argc != 3){usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);int sock = 0;sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" < connectstruct 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());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) // 比UDP多了这一步{std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl; // 到这链接成功了while (true){std::string line;std::cout < 0){buffer[s] = 0;std::cout << "server 回显# " << buffer << std::endl;}else // 关闭链接或者读取失败{break;}}close(sock);return 0;}*/

编译运行:

此时链接的时候没有把回显信息打印出来,不过先不改了。下面再弄一个类似英汉互译的:

static void dictOnline(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name){//一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接char buffer[1024];static std::unordered_map dict = {{"apple", "苹果"},{"watermelon", "西瓜"},{"banana", "香蕉"},{"hard", "好难"}};// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer <second;write(sock, message.c_str(), message.size());}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));}close(sock);}

编译运行:

完成运行,为了方便这里再放下tcp_server.hpp。

tcp_server.hpp

#pragma once#include #include #include #include #include #include #include #include  // 网络四件套#include #include #include #include #include #include #include "log.hpp"#include "threadPool.hpp"#include "Task.hpp"#include // static void service(int sock, const std::string &clientip, const uint16_t &clientport)// {// //echo server// char buffer[1024];// while(true)// {// // read && write 可以直接被使用// ssize_t s = read(sock, buffer, sizeof(buffer)-1);// if(s > 0)// {// buffer[s] = 0; // 将发过来的数据当做字符串// std::cout << clientip << ":" << clientport << "# " << buffer < 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer < 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;std::string message;char *start = buffer;while(*start){char c;if(islower(*start)) {c = toupper(*start);}else{c = *start;}message.push_back(c);++start;}write(sock, message.c_str(), message.size());}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));}close(sock);}static void dictOnline(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name){//一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接char buffer[1024];static std::unordered_map dict = {{"apple", "苹果"},{"watermelon", "西瓜"},{"banana", "香蕉"},{"hard", "好难"}};// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer <second;write(sock, message.c_str(), message.size());}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);}else{logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));}close(sock);}// class ThreadData// {// public:// int _sock;// std::string _ip;// uint16_t _port;// };class TcpServer{protected:const static int gbacklog = 20;// listen的第二个参数,现在先不管// static void *threadRoutine(void *args) // 加上static就没this指针了// {// pthread_detach(pthread_self()); // 线程分离// ThreadData *td = static_cast(args);// service(td->_sock, td->_ip, td->_port);// delete td;// return nullptr;// }public:TcpServer(uint16_t port, std::string ip=""):_listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool::getThreadPool()){}void initServer(){// 1. 创建socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif(_listensock  用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号// signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态_threadpool_ptr->run();while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接// servicesock(服务套接字,相当于此小区域专门给你负责的) // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)if(servicesock  要从单进程改成多线程// service(servicesock, client_ip, client_port);// // 2 version 2.0 -- 多进程版 --- 创建子进程// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能// pid_t id = fork();// assert(id != -1);// if(id == 0) // 子进程// {// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要// close(_listensock); // 关闭自己不需要的套接字// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务// exit(0); // 僵尸状态// }// // 父进程// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少// // 如果父进程关闭servicesock,会不会影响子进程?// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号// // version2.1 -- 多进程版 -- version 2.0 改版// pid_t id = fork();// if(id == 0)// {// // 子进程// close(_listensock);// if(fork() > 0)// 再创建子进程,子进程本身// exit(0); //子进程本身立即退出// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程// service(servicesock, client_ip, client_port);// exit(0);// }// // 父进程// waitpid(id, nullptr, 0); // 不会阻塞// close(servicesock);// // version 3 --- 多线程版本// // 创建进程的成本是很高的,所以再改成多线程版(不封装了)// ThreadData *td = new ThreadData();// td->_sock = servicesock;// td->_ip = client_ip;// td->_port = client_port;// pthread_t tid;// // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的// pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类// verison4 --- 线程池版本// Task t(servicesock, client_ip, client_port, service);// Task t(servicesock, client_ip, client_port, change);Task t(servicesock, client_ip, client_port, dictOnline);_threadpool_ptr->pushTask(t);}}~TcpServer(){}protected:uint16_t _port;std::string _ip;int _listensock;std::unique_ptr<ThreadPool> _threadpool_ptr;};

2. 笔试选择题

1. 在网络字节序中,所谓”小端”(little endian)说法正确的是( )

A.高字节数据存放在低地址处,低字节数据存放在高地址处

B.低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处

C.和编译器相关

D.上述答案都不正确

2.当一个UDP报文道达目的主机时,操作系统使用()选择正确的socket.

A.源IP地址

B.源端口号

C.目的端口号

D.目的IP地址

3.以下有关于端口号的说法错误的是()

A.tcp的最大端口号是65535

B.端口号是一个2字节16位的整数

C.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程

D.一个进程至多只能绑定一个端口

4.【多选题】socket编程中经常需要进行字节序列的转换,下列哪几个函数是将网络字节序列转换为主机字节序列?()

A.htons

B.ntohs

C.htonl

D.ntohl

5.【多选题】Socket,即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口。socket的使用类型主要有()

A.基于TCP协议,采用流方式,提供可靠的字节流服务

B.基于IP协议,采用流数据提供数据网络发送服务

C.基于HTTP协议,采用数据包方式提供可靠的数据包装服务

D.基于UDP协议,采用数据报方式提供数据打包发送服务

6.下列有关Socket的说法,错误的是()

A.Socket用于描述IP地址和端口,是一个通信链的句柄

B.Socket通信必须建立连接

C.Socket客户端的端口是不固定的

D.Socket服务端的端口是固定的

答案及解析

1. B

小端:低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处

大端:高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址

网络字节序为大端字节序

2. C

IP地址用于标识主机,端口号用于标识主机上的对应网络通信socket

因此正确选项为C选项,通过目的端口号来区分是哪个socket

3. D

端口号是一个无符号16位的整数,用于表示主机上的网络通信socket,因为是16位,因此最大端口号是65535(从0开始)

IP地址可以在网络当中标识一台主机,端口号可以在主机当中标识一个进程(socket会关联到对应进程)

一个进程当中对于端口的绑定是和socket强相关,理论上该进程如果创建多个socket,可以给每一个socket都进行绑定一个端口

基于以上理解,错误选项为:D (一个进程可能会创建多个socket,因此有可能会绑定多个端口)

4. BD

函数名称解析:

  • n 对应是 network
  • h 对应是 host
  • s 对应是 short
  • l 对应是 long

因此网络字节序到主机字节序的转换是 ntoh

5. AD

TCP协议,采用流方式,SOCK_STREAM, 可靠

UDP协议,采用数据报方式,SOCK_DGRAM, 不可靠

HTTP协议是应用层协议,在传输层基于TCP协议实现, (可靠是TCP提供的,TCP提供字节流传输)

IP协议是网络层协议,TCP和UDP协议在网络层都是基于IP协议的。(实现数据报传输)

6. B

A正确:概念性理解,socket就是一条通信的句柄

B错误:socket 可以基于TCP 面向连接 也可以基于UDP无连接

C正确:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定

D正确:服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了


本篇完。

下一篇:网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)。