赞
踩
目录
因为,TCP面向字节流,所以TCP有粘包问题,故我们需要应用层协议来区分每一个数据包。防止读取到半个,一个半数据包的情况。
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include "log.hpp"
-
- // 对于一些TCP相关调用的封装
- class Sock
- {
- private:
- const static int gback_log = 20;
- public:
- int Socket()
- {
- // 1. 创建套接字,成功返回对应套接字,失败直接进程exit
- int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 网络套接字, 面向字节流(tcp)
- if (listen_sock < 0)
- {
- logMessage(FATAL, "create listen socket error, %d:%s", errno, strerror(errno));
- exit(2);
- }
- logMessage(NORMAL, "create listen socket success: %d", listen_sock); // 1111Log
- return listen_sock;
- }
- void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
- {
- // 2. bind,注意云服务器不能绑定公网IP,不允许。
- // 成功bind则成功bind,失败进程exit(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 = inet_addr(ip.c_str());
- if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
- {
- logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
- exit(3);
- }
- }
- void Listen(int sock)
- {
- // 3. listen监听: 因为TCP是面向连接的,在我们正式通信之前,需要先建立连接
- // listen: 将套接字状态设置为监听状态。服务器要一直处于等待状态,这样客户端才能随时随地发起连接。
- // 成功则成功,失败则exit
- if (listen(sock, gback_log) < 0) // gback_log后面讲,全连接队列的长度。我猜测就是这个服务器同一时刻允许连接的客户端的数量最大值?也不太对呀,这么少么?
- {
- logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
- exit(4);
- }
- logMessage(NORMAL, "listen success");
- }
- // 一般经验
- // const std::string &: 输入型参数
- // std::string *: 输出型参数
- // std::string &: 输入输出型参数
- int Accept(int sock, uint16_t *port, std::string *ip)
- {
- // accept失败进程不退出,返回-1
- // 成功则返回对应的通信套接字
- struct sockaddr_in client;
- socklen_t len = sizeof client;
- // 其实accept是获取已经建立好的TCP连接。建立好的连接在一个内核队列中存放,最大数量的第二个参数+1
- int service_sock = accept(sock, (struct sockaddr *)&client, &len); // 返回一个用于与客户端进行网络IO的套接字,不同于listen_sock
- // On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
- if (service_sock < 0)
- {
- logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
- return -1; // accept失败不直接exit,而是返回-1。因为在循环语句内部。
- }
- if (port)
- *port = ntohs(client.sin_port);
- if (ip)
- *ip = inet_ntoa(client.sin_addr);
- logMessage(NORMAL, "link(accept) success, service socket: %d | %s:%d", service_sock,
- (*ip).c_str(), *port);
- return service_sock;
- }
- int Connect(int sock, const std::string &ip, const uint16_t &port)
- {
- // 惯例写一下:失败返回-1,成功则客户端与服务端连接成功,返回0
- struct sockaddr_in server;
- memset(&server, 0, sizeof server);
- server.sin_family = AF_INET;
- server.sin_addr.s_addr = inet_addr(ip.c_str());
- server.sin_port = htons(port);
- if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
- {
- return -1;
- }
- return 0;
- }
- public:
- Sock() = default;
- ~Sock() = default;
- };
- #ifndef _TCP_SERVER_HPP_
- #define _TCP_SERVER_HPP_
-
- #include "Sock.hpp"
- #include <vector>
- #include <functional>
-
- // 说实话,这个TcpServer类实现的非常棒,真的很棒,网络和服务进行了解耦。
- // 使用者直接BindServer, 然后start即可
- namespace ns_tcpserver
- {
- using func_t = std::function<void(int socket)>; // 服务器提供的服务方法类型void(int),可变
-
- class TcpServer;
- class ThreadData
- {
- public:
- ThreadData(int sock, TcpServer *server)
- : _sock(sock), _server(server)
- {}
- ~ThreadData() {}
- public:
- int _sock;
- TcpServer *_server; // 因为静态成员函数呀
- };
-
- class TcpServer
- {
- // 不关心bind的ip和port,因为用不到啊,保留一个listen_sock用于accept就够了。
- private:
- int _listen_sock;
- Sock _sock;
- std::vector<func_t> _funcs; // 服务器提供的服务
- private:
- static void *threadRoutine(void *args)
- {
- pthread_detach(pthread_self()); // 线程分离(避免类似于僵尸进程状态)
- ThreadData *td = (ThreadData *)args;
- td->_server->excute(td->_sock); // 提供服务
- close(td->_sock); // 保证四次挥手正常结束
- delete td;
- return nullptr;
- }
- public:
- TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
- {
- // 创建监听套接字,bind,listen
- _listen_sock = _sock.Socket();
- _sock.Bind(_listen_sock, port, ip);
- _sock.Listen(_listen_sock);
- }
- void start()
- {
- for (;;)
- {
- // 开始accept,然后执行任务
- std::string ip;
- uint16_t port; // 这两个东西,也并没有传给线程。
- int sock = _sock.Accept(_listen_sock, &port, &ip); // 后面是输出型参数
- if (sock == -1)
- continue; // 本次accept失败,循环再次accept。目前来看几乎不会
-
- // 连接客户端成功,ip port已有。但是这里没用...
- pthread_t tid;
- ThreadData *td = new ThreadData(sock, this);
- pthread_create(&tid, nullptr, threadRoutine, (void *)td); // 新线程去提供service,主线程继续accept
- }
- }
- void bindService(func_t service) // 暴露出去的接口,用于设置该服务器的服务方法
- {
- _funcs.push_back(service);
- }
- void excute(int sock)
- {
- for (auto &func : _funcs)
- {
- func(sock);
- }
- }
- ~TcpServer()
- {
- if (_listen_sock >= 0)
- close(_listen_sock);
- }
- };
- }
- #endif
- #ifndef _PROTOCOL_HPP_
- #define _PROTOCOL_HPP_
-
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include <jsoncpp/json/json.h>
-
- // important and new
- namespace ns_protocol
- {
-
- // #define MYSELF 1 // 自己实现序列化反序列化还是使用json库
-
- #define SPACE " "
- #define SPACE_LENGTH strlen(SPACE)
- #define SEP "\r\n"
- #define SEP_LENGTH strlen(SEP)
-
- // 请求和回复,都需要序列化和反序列化的成员函数
- // 序列化和反序列化双方都不同。但是添加报头和去报头是相同的,"Length\r\nxxxxx\r\n";
- // 客户端生成请求,序列化之后发送给服务端
- class Request
- {
- public:
- Request() = default;
- Request(int x, int y, char op)
- : _x(x), _y(y), _op(op)
- {
- }
- ~Request() {}
-
- public:
- int _x;
- int _y;
- char _op;
-
- public:
- std::string serialize()
- {
- // 序列化为"_x _op _y" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
- #ifdef MYSELF
- std::string s = std::to_string(_x);
- s += SPACE;
- s += _op;
- s += SPACE;
- s += std::to_string(_y);
- return s;
- #else
- Json::Value root;
- root["x"] = _x;
- root["y"] = _y;
- root["op"] = _op;
- Json::FastWriter writer;
- return writer.write(root);
- #endif
- }
- bool deserialize(const std::string &s)
- {
- #ifdef MYSELF
- // "_x _op _y"
- std::size_t left = s.find(SPACE);
- if (left == std::string::npos)
- return false;
- std::size_t right = s.rfind(SPACE);
- if (right == left)
- return false;
- _x = atoi(s.substr(0, left).c_str());
- _op = s[left + SPACE_LENGTH];
- _y = atoi(s.substr(right + SPACE_LENGTH).c_str());
- #else
- Json::Value root;
- Json::Reader reader;
- reader.parse(s, root);
- _x = root["x"].asInt();
- _y = root["y"].asInt();
- _op = root["op"].asInt();
- #endif
- return true;
- }
- };
- // 服务端收到请求,反序列化,业务处理生成response,序列化后发送给客户端
- class Response
- {
- public:
- Response(int result = 0, int code = 0)
- : _result(result), _code(code)
- {
- }
- ~Response() {}
-
- public:
- std::string serialize()
- {
- // 序列化为"_code _result" (注意,序列化和添加报头是分开的,反序列化和去掉报头是分开的
- #ifdef MYSELF
- std::string s = std::to_string(_code);
- s += SPACE;
- s += std::to_string(_result);
- return s;
- #else
- Json::Value root;
- root["code"] = _code;
- root["result"] = _result;
- Json::FastWriter writer;
- return writer.write(root);
- #endif
- }
- bool deserialize(const std::string &s)
- {
- #ifdef MYSELF
- // "_code _result"
- std::size_t pos = s.find(SPACE);
- if (pos == std::string::npos)
- return false;
- _code = atoi(s.substr(0, pos).c_str());
- _result = atoi(s.substr(pos + SPACE_LENGTH).c_str());
- #else
- Json::Value root;
- Json::Reader reader;
- reader.parse(s, root);
- _result = root["result"].asInt();
- _code = root["code"].asInt();
- #endif
- return true;
- }
- public:
- int _result;
- int _code; // 状态码, 防止除零,模零,和其他错误(比如非法运算符运算符)。code == 0时,result有效。
- };
- // 进行去报头,报文完整则去报头,并返回有效载荷,不完整则代表失败返回空字符串。
- std::string deCode(std::string &s) // 输入型输出型参数
- {
- // "Length\r\nx op y\r\n" 成功返回有效载荷,失败返回空串
- std::size_t left = s.find(SEP);
- if (left == std::string::npos)
- return "";
- std::size_t right = s.rfind(SEP);
- if (right == left)
- return "";
- int length = atoi(s.substr(0, left).c_str());
- if (length > s.size() - left - 2 * SEP_LENGTH)
- return ""; // 有效载荷长度不足,不是一个完整报文,其实经过上面两次的if判断已经够了可能。
- // 是一个完整报文,进行提取
- std::string ret;
- s.erase(0, left + SEP_LENGTH);
- ret = s.substr(0, length);
- s.erase(0, length + SEP_LENGTH);
- return ret;
- }
- std::string enCode(const std::string &s)
- {
- // "Length\r\n1+1\r\n"
- std::string retStr = std::to_string(s.size());
- retStr += SEP;
- retStr += s;
- retStr += SEP;
- return retStr;
- }
- // 我真的很想用引用,但是好像传统规则是输出型参数用指针...
- // 其实这个Recv就是一个单纯的读数据的函数,将接收缓冲区数据读到应用层缓冲区中,也就是*s中。存储的是对端发来的应用层报文。
- bool Recv(int sock, std::string *s)
- {
- // 仅仅读取数据到*s中
- char buff[1024];
- ssize_t sz = recv(sock, buff, sizeof buff, 0);
- if (sz > 0)
- {
- buff[sz] = '\0';
- *s += buff;
- return true;
- }
- else if (sz == 0)
- {
- std::cout << "peer quit" << std::endl;
- return false;
- }
- else
- {
- std::cout << "recv error" << std::endl;
- return false;
- }
- }
- bool Send(int sock, const std::string &s)
- {
- ssize_t sz = send(sock, s.c_str(), s.size(), 0);
- if (sz > 0)
- {
- return true;
- }
- else
- {
- std::cout << "send error!" << std::endl;
- return false;
- }
- }
- }
-
- #endif
- #include "TcpServer.hpp"
- #include "Protocol.hpp"
- #include <memory>
-
- using namespace ns_tcpserver;
- using namespace ns_protocol;
-
- Response calculatorHelp(const Request &req)
- {
- // "1+1"???
- Response resp;
- int x = req._x;
- int y = req._y;
- switch (req._op)
- {
- case '+':
- resp._result = x + y;
- break;
- case '-':
- resp._result = x - y;
- break;
- case '*':
- resp._result = x * y;
- break;
- case '/':
- if (y == 0)
- resp._code = 1;
- else
- resp._result = x / y;
- break;
- case '%':
- if (y == 0)
- resp._code = 2;
- else
- resp._result = x % y;
- break;
- default:
- resp._code = 3;
- break;
- }
- return resp;
- }
-
- void calculator(int sock)
- {
- std::string s;
- for (;;)
- {
- if (Recv(sock, &s) <= 0) // 输出型参数
- break; // 大概率对端退出,则服务结束。一般不会读取失败recv error
- std::string package = deCode(s);
- if (package.empty())
- continue; // 不是一个完整报文,继续读取(因为TCP面向字节流!!!)
- // 读取到一个完整报文,且已经去了应用层报头,有效载荷在package中。如"1 + 2"
- Request req;
- req.deserialize(package);
- Response resp = calculatorHelp(req);
- std::string backStr = resp.serialize();
- backStr = enCode(backStr);
- if (!Send(sock, backStr)) // 发送失败就退出
- break;
- }
- }
-
- // ./cal_server port
- int main(int argc, char **argv)
- {
- // std::cout << "test remake" << std::endl; // success
- if (argc != 2)
- {
- std::cout << "\nUsage: " << argv[0] << " port\n"
- << std::endl;
- exit(1);
- }
- std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
- server->bindService(calculator); // 给服务器设置服务方法,将网络服务和业务逻辑进行解耦
- server->start(); // 服务器开始进行accept,连接一个client之后就提供上方bind的服务
- return 0;
- }
- #include "Protocol.hpp"
- #include "Sock.hpp"
- #include <memory>
-
- using namespace ns_protocol;
-
- // ./client serverIp serverPort
- int main(int argc, char **argv)
- {
- if (argc != 3)
- {
- std::cout << "\nUsage: " << argv[0] << " serverIp serverPort\n"
- << std::endl;
- exit(1);
- }
- Sock sock;
- int sockfd = sock.Socket();
- // 客户端不需要显式bind, 老生常谈了。
- if (sock.Connect(sockfd, argv[1], atoi(argv[2])) == -1)
- {
- std::cout << "connect error" << std::endl;
- exit(3);
- }
- std::string backStr; //
- bool quit = false;
- while (!quit)
- {
- Request req;
- std::cout << "Please enter# ";
- std::cin >> req._x >> req._op >> req._y;
- std::string reqStr = req.serialize();
- reqStr = enCode(reqStr); // 添加应用层报头,此处添加报头(制定协议)是为了解决TCP粘包问题,因为TCP是面向字节流的。
- if (!Send(sockfd, reqStr))
- break;
- while (true)
- {
- if (!Recv(sockfd, &backStr))
- {
- quit = true;
- break;
- }
- std::string package = deCode(backStr);
- if(package.empty())
- continue; // 这次不是一个完整的应用层报文,继续读取
- // 读取到一个完整的应用层报文,且已经去报头,获取有效载荷成功,在package中。(这个有效载荷是server发来的,计算结果)
- Response resp;
- resp.deserialize(package);
- switch (resp._code)
- {
- case 1:
- std::cout << "除零错误" << std::endl;
- break;
- case 2:
- std::cout << "模零错误" << std::endl;
- break;
- case 3:
- std::cout << "其他错误" << std::endl;
- break;
- default:
- std::cout << req._x << " " << req._op << " " << req._y << " = " << resp._result << std::endl;
- break;
- }
- break; // 退出防止TCP粘包问题的循环。
- }
- // 进行下一次获取用户输入,进行计算。
- }
- close(sockfd);
- return 0;
- }
可以分为两个模块:网络通信模块,应用层模块(包括应用层协议,以及应用层计算器逻辑)。
网络模块中,Sock.hpp就是一个简单的对于系统调用的封装,TcpServer.hpp的设计很优雅,内部有一个std::vector<func_t> _funcs;即这个server提供的服务。对外提供一个BindServer的方法,可以指定这个服务器提供的服务。Start,为服务器开始方法,先accept获取与客户端建立好的连接,然后创建新线程给客户端提供服务,服务就是BindServer绑定的方法,类型为void(int)。
应用层协议:一个Request,一个Response。分别是客户端的请求(x,y,运算符)和服务端的响应(计算结果)。这两个类,都有序列化和反序列化的方法,便于网络传输。还有一个Encode添加报头和Decode去报头的方法,这个其实就是应用层协议的报头,大体格式为 Length\r\nxxxx\r\n。目的就是解决TCP面向字节流所引起的粘包问题。
Recv内部就是一个recv调用,将读取的网络数据添加到一个输出型参数string*指向string的结尾。因为TCP粘包问题,所以可能读取的不是一个完整报文(半个?),故,在Decode方法内部,也就是去报头时,会检测此时是否有至少一个完整应用层报文。若有,则去报头,获取有效载荷。若没有则返回一个空串。上层可以通过判断是否为空串。判断是否读到了一个完整应用层报文,若没有,则再次Recv,直到读到一个完整应用层报文为止。所以,server和client在读取网络数据并去报头时,都是在while循环内部进行的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。