4.4 🍎udpServer.c🍎
#include<memory> #include"udpServer.hpp" string dealMessage(const string& message) { return message; } void usage() { cout<<"Usage error\n\t"<<"serverPort"<<endl; exit(0); } //./udpServer serverPort int main(int argc,char* argv[]) { if(argc!=2) { usage(); } unique_ptr<udpServer> udpSer(new udpServer(dealMessage,8848)); udpSer->init(); udpSer->start(); return 0; }
上述准备工作做好了后就可以来上手验证了:
注意我们在运行客户端的可执行程序时加上的IP地址可以直接是127.0.0.1
(表示本机),如果想要其他主机也能够正确访问的话要加上服务端的IP,也就是我们购买云服务器的公网IP地址。
4.5 🍎如何关闭防火墙+验证🍎
如果使用了云服务器的公网IP地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:
最后点击确认,就可以了,我们就发现列表中多出了两条:
到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:
这样我们就完成了一个简易版本的UDP网络通信的代码了。
除此之外,我们还可以实现一个客户端把命令给服务端,然后服务端在帮助我们执行:
static bool isPass(const std::string &command) { bool pass = true; auto pos = command.find("rm"); if(pos != std::string::npos) pass=false; pos = command.find("mv"); if(pos != std::string::npos) pass=false; pos = command.find("while"); if(pos != std::string::npos) pass=false; pos = command.find("kill"); if(pos != std::string::npos) pass=false; return pass; } // 让客户端本地把命令给服务端,server再把结果给你! // ls -a -l std::string excuteCommand(std::string command) // command就是一个命名 { // 1. 安全检查 if(!isPass(command)) return "you are bad man!"; // 2. 业务逻辑处理 FILE *fp = popen(command.c_str(), "r"); if(fp == nullptr) return "None"; // 3. 获取结果了 char line[1024]; std::string result; while(fgets(line, sizeof(line), fp) != NULL) { result += line; } pclose(fp); return result; }
当我们运行时:
不难发现已经验证成功了。
上述代码中我们简单介绍下popen
函数:
这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)
当然在客户端和服务端中我们修改代码为生产者消费者模型(具体实现可以让一个线程读取消息,另外一个线程收消息)由于同一个文件描述符可以同时被多个线程读取,所以这样设计是OK的。这里我就不实验了,大家有兴趣可以自行下去尝试。
5 🍑简单的TCP网络程序 🍑
TCP的网络程序大致框架与UDP类似,其中不同点我会放在后面一点一点给出解释。
5.1 🍎tcpServer.hpp(重要)🍎
#pragma once #include "err.hpp" #include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <string> #include <functional> #include <unistd.h> #include <string> #include <cerrno> #include <cstring> #include<signal.h> using namespace std; using func_t = function<string(const string &)>; static const int backlog = 32; class tcpServer { public: tcpServer(func_t func, uint16_t port) : _func(func), _port(port) { } void init() { // 1 创建套接字 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { cerr << "creat sock fail:" << strerror(errno) << endl; exit(SOCK_ERR); } // 2 bind 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, (sockaddr *)&local, sizeof(local)) < 0) { cerr << "bind fail" << endl; exit(BIND_ERR); } // 3 listen if (listen(_listensock, backlog) < 0) { cerr << "listen fail" << strerror(errno) << endl; exit(LISTEN_ERR); } } void service(int sock, const string &clientip, const uint16_t &clientport) { string who = clientip + "-" + std::to_string(clientport) + ":"; char buffer[1024]; while (true) { // 1 读取消息 ssize_t n = read(sock, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = 0; // 2 处理消息 string message = _func(buffer); cout << who << message << endl; // server 发送消息给 client int n = write(sock, message.c_str(), message.size()); if (n < 0) { cerr << "write fail" << strerror(errno) << endl; exit(WRITE_ERR); } } else if (n == 0) { cout << "client:" << clientip << "-" << to_string(clientport) << "quit,server also quit" << endl; close(sock); } else { cerr << "read fail" << strerror(errno) << endl; exit(READ_ERR); } } } void start() { while (true) { // 1 获取连接 明确是哪一个client发送来的 sockaddr_in client; socklen_t len; int sock = accept(_listensock, (sockaddr *)&client, &len); if (sock < 0) { cerr << "accept fail" << strerror(errno) << endl; continue; } std::string clientip = inet_ntoa(client.sin_addr); uint16_t clientport = ntohs(client.sin_port); cout << "get new link success:" << sock << " form " << _listensock << endl; // 2 处理消息 service(sock, clientip, clientport); private: int _listensock; uint16_t _port; func_t _func; };
5.1.1 🍋注意事项🍋
1️⃣由于是TCP,所以我们创建套接字时必须使用SOCK_STREAM.
2️⃣由于TCP是保证可靠性的面向字节流的可靠协议,所以TCP在使用上肯定会比UDP复杂得多,会多上listen(监听) 和 accept (获取连接)。在linten接口的创建中我们使用的第二个参数backlog我们将放在后面再讲解,这里不太好解释。accept接口的返回值也是一个套接字,这个套接字的任务是专门用来帮助我们读取和接受消息用的,而类中的_listensock套接字的作用主要是进行前面套接字的创建和初始化工作。(可以简单的理解为_listensock就相当于餐厅里在外面招呼客人的服务员,accept接口的返回值套接字就是为客户真正意义上做饭的厨师)
3️⃣我们将处理消息封装在了一个接口service中,在里面我们可以清晰得看见,读取消息用的是read,发送消息用的是write,这正是我们学习文件操作时所用到得系统调用,这也很好的印证在LINUX下一切皆文件的思想。
4️⃣ 代码中所存在的错误都用了错误码来标识,错误码可参考下面:
enum { SOCK_ERR=1, BIND_ERR, USAGE_ERR, LISTEN_ERR, ACCEPT_ERR, CONNECT_ERR, WRITE_ERR, READ_ERR, };
tcpClient.cc:
#pragma once #include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <functional> #include <unistd.h> #include <string> #include <cerrno> #include <cstring> #include "err.hpp" using namespace std; static void usage(string proc) { std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; } int main(int argc,char*argv[]) { if(argc!=3) { usage(argv[0]); exit(USAGE_ERR); } // 1 创建套接字 int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { cerr << "creat sock fail:" << strerror(errno) << endl; exit(SOCK_ERR); } //2 client要bind,但是是不需要我们自己bind的 //client需要listen和accept吗?答案是不需要的 //3 connect string serverip=argv[1]; uint16_t serverport=stoi(argv[2]); 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)); int cnt = 5; while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0) { sleep(1); cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl; if(cnt <= 0) break; } if(cnt <= 0) { cerr << "连接失败..." << endl; exit(CONNECT_ERR); } char buffer[1024]; // 3. 连接成功 while(true) { string line; cout << "Enter>>> "; getline(cin, line); write(sock, line.c_str(), line.size()); ssize_t s = read(sock, buffer, sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; cout << "server echo >>>" << buffer << endl; } else if(s == 0) { cerr << "server quit" << endl; break; } else { cerr << "read error: " << strerror(errno) << endl; break; } } close(sock); return 0; }
5.2 🍎tcpClient.cc🍎
#pragma once #include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <functional> #include <unistd.h> #include <string> #include <cerrno> #include <cstring> #include "err.hpp" using namespace std; static void usage(string proc) { std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; } int main(int argc,char*argv[]) { if(argc!=3) { usage(argv[0]); exit(USAGE_ERR); } // 1 创建套接字 int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { cerr << "creat sock fail:" << strerror(errno) << endl; exit(SOCK_ERR); } //2 client要bind,但是不需要我们自己bind的 //client需要listen和accept吗?答案是不需要的 //3 connect string serverip=argv[1]; uint16_t serverport=stoi(argv[2]); 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)); int cnt = 5; while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0) { sleep(1); cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl; if(cnt <= 0) break; } if(cnt <= 0) { cerr << "连接失败..." << endl; exit(CONNECT_ERR); } char buffer[1024]; // 3. 连接成功 while(true) { string line; cout << "Enter>>> "; getline(cin, line); write(sock, line.c_str(), line.size()); ssize_t s = read(sock, buffer, sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; cout << "server echo >>>" << buffer << endl; } else if(s == 0) { cerr << "server quit" << endl; break; } else { cerr << "read error: " << strerror(errno) << endl; break; } } close(sock); return 0; }
5.2.1 🍋注意事项🍋
- 1️⃣与UDP类似在bind的时候需要bind,但是这个工作不由我们自己完成,而是由OS来完成。
- 2️⃣在客户端是不用
listen
和accept
的,但是需要connect
(建立连接)我们可以自定义连接策略(失败了重连几次)。
5.3 🍎tcpServer.cc🍎
#include<memory> #include"err.hpp" #include"tcpServer.hpp" string echoMssage(const string& message) { return message; } static void usage(string proc) { std::cout << "Usage:\n\t" << proc << " port\n" << std::endl; } int main(int argc,char*argv[]) { if(argc!=2) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port=stoi(argv[1]); unique_ptr<tcpServer> utcp(new tcpServer(echoMssage,port)); utcp->init(); utcp->start(); return 0; }
5.4 🍎验证🍎
我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?
我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。
当我们把最先通信的客户端干掉之后:
消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。
来看看我们写的代码:
当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在service中我们是死循环的读取和发送消息的,那么当有另外的客户端请求时就不会给新的客户端建立连接,自然就发不出去,收不到喽!处理方式有两种:
- 多进程
- 多线程
5.4.1 🍋多进程🍋
void start() { while (true) { // 1 获取连接 明确是哪一个client发送来的 sockaddr_in client; socklen_t len; int sock = accept(_listensock, (sockaddr *)&client, &len); if (sock < 0) { cerr << "accept fail" << strerror(errno) << endl; continue; } std::string clientip = inet_ntoa(client.sin_addr); uint16_t clientport = ntohs(client.sin_port); cout << "get new link success:" << sock << " form " << _listensock << endl; // 2 处理消息 //service(sock, clientip, clientport); // 这样做当我们有多个client时会有什么问题? // 方案一:多进程 让子进程帮助我们执行service pid_t pid = fork(); if (pid < 0) { close(sock); continue; } else if (pid == 0) { // child 建议关掉_listensock close(_listensock); service(sock, clientip, clientport); exit(0); } // parent 一定要关闭sock,否则就会造成文件描述符的泄漏 close(sock); waitpid(id, nullptr, WNOHANG); if (ret == pid) std::cout << "wait child " << pid << " success" << std::endl; }
这样我们就能够很好的处理了。
除此之外还有一种更为精妙的方式:
我们可以再fork一下,当是父进程的时候就退出,执行到下面那肯定就是孙子进程,由OS领养,自然就不用关心回收状态了(OS会自动帮助我们回收)
当然,这还不是最好的方式,最好的方式我们可以使用下面的代码:
signal(SIGCHLD, SIG_IGN); // 推荐这样写
一行就搞定了,直接忽略掉子进程退出给父进程发送的消息。
5.4.2 🍋多线程🍋
// 方案二:多线程 pthread_t pid; TcpData *pdata = new TcpData(sock, clientip, clientport, this); pthread_create(&pid, nullptr, threadRoutine, pdata); } } static void *threadRoutine(void *args) { pthread_detach(pthread_self()); TcpData* pd=static_cast<TcpData*>(args); pd->_cur->service(pd->_sock,pd->_clientip,pd->_clientport); }
其中TcpData类:
class tcpServer; class TcpData { public: TcpData(int sock, string &_clientip, uint16_t _clientport, tcpServer *cur) : _sock(sock), _clientip(_clientip), _clientport(_clientport), _cur(cur) { } int _sock; string _clientip; uint16_t _clientport; tcpServer *_cur; };
这样当我们再次运行时:
显然此时已经能够成功运行了。除了服务端使用多线程外,客户端也可以用一个线程池来创建,总的来说实现起来这里也不算太难,有兴趣的小伙伴可以参考博主之前实现的【Linux:线程池】来改装一下,有问题可以私信博主。
6 🍑TCP协议通讯流程🍑
这张图大家目前应该是看不太明白的,其实没啥关系,上面讲解的内容在博主后面的文章中会给出详细的解释,这里大家只需要简单的了解下过程就好了。
服务器初始化:
调用socket, 创建文件描述符;
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
调用accecpt, 并阻塞, 等待客户端连接过来。
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手。
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手。
在学习socket API时要注意应用程序和TCP协议层是如何交互的?
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。