通信双方在进行网络通信的时候:
如果要传输的数据是一个字符串 那么通信双方直接发送即可
如果要传输的数据是一些结构体 此时就不能将这些数码一个个发送到网络中
如果要实现一个网络版本的计算器,那么客户端每次发送的请求就需要包括左操作数, 右操作数, 和对应的操作 。那么此时客户端要发送的就不是一个简单的字符串 ,而是一个结构体。
如果客户端将这些结构化的数据单独一个个发送到网络中, 那么服务端也只能一个个的接受, 但是这样子传输容易导致数据错乱。
所以客户端往往会将这些结构化的数据统一打包发送到网络中 ,此时服务端接受的就是一个完整的请求了。
我们还是以我们要设计的计算器为例子。
这个字符串中有两个操作数 都是整型
两个数字之间会有一个字符是运算符
数字和运算符之间没有空格
客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中 。此时服务端每次从网络当中获取到的就是这样一个字符串 ,然后服务端再以相同的方式对这个字符串进行解析, 此时服务端就能够从这个字符串当中提取出这些结构化的数据。
定制结构体来表示我们想要传递的信息
发送数据时将这个结构体按照一个规则转换成网络标准数据格式 接收数据时再按照相同的规则把接收到的数据转化为结构体
这个过程我们就叫做序列化和反序列化
客户端发送数据时先对数据进行序列化 ,服务端接收到数据后再对其进行反序列化 ,此时服务端就能得到客户端发送过来的结构体 ,进而从该结构体当中提取出对应的信息。
首先我们需要对服务器进行初始化:
初始化完服务器后就可以启动服务器了 服务器启动后要做的就是不断调用accept函数 从监听套接字当中获取新连接 每当获取到一个新连接后就创建一个新线程 让这个新线程为该客户端提供计算服务
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main(int argc , char* argv[])
{
if (argc != 2)
{
cout << "usage error" << endl;
exit(1);
}
// port socket
int port = atoi(argv[1]);
int listen_sock = socket(AF_INET , SOCK_STREAM , 0);
if (listen_sock < 0)
{
cout << "socket error" << endl ;
exit(2);
}
struct sockaddr_in local;
memset(&local , 0 , sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock , (struct sockaddr*)&local , sizeof(local)) < 0)
{
cout << "bind error" << endl;
exit(3);
}
if (listen(listen_sock , 5) < 0)
{
cout << "listen error" << endl;
exit(4);
}
struct sockaddr_in peer;
memset(&peer , '\0' , sizeof(peer));
socklen_t len;
for(;;)
{
int sock = accept(listen_sock , (struct sockaddr*)&peer , &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue; // do not stop
}
pthread_t tid;
int* p = new int(sock);
E> pthread_create(&tid , nullptr , Rontinue , (void*) p)
}
return 0;
}
说明一下:
要实现一个网络版的计算器 就必须保证通信双方能够遵守某种协议约定 因此我们需要设计一套简单的约定 数据可以分为请求数据和响应数据 因此我们分别需要对请求数据和响应数据进行约定
在实现时可以采用C++当中的类来实现 也可以直接采用结构体来实现 这里就使用结构体来实现 此时就需要一个请求结构体和一个响应结构体
规定状态字段对应的含义:
typedef struct request
{
int left;
int right;
char op;
}request_t;
typedef struct response
{
int code;
int result;
}response_t;
123456789101112
要注意的是作为一种约定 它必须要被通信的双方所知晓 也就是说 要么我们将这个协议写在一个头文件中并同时包含在客户端和服务端中 要么在客户端和服务端都写上这么一段相同的代码
客户端首先也需要进行初始化:
客户端初始化完毕后需要调用connect函数连接服务端 当连接服务端成功后 客户端就可以向服务端发起计算请求了 这里可以让用户输入两个操作数和一个操作符构建一个计算请求 然后将该请求发送给服务端 而当服务端处理完该计算请求后 会对客户端进行响应 因此客户端发送完请求后还需要读取服务端发来的响应数据
int main(int argc , char* argv[])
{
if (argc != 3)
{
cerr << "usage error" << endl;
exit(1);
}
string ip = argv[1];
int port = atoi(argv[2]);
int sockfd = socket(AF_INET , SOCK_STREAM , 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
exit(2);
}
struct sockaddr_in peer;
memset(&peer , '\0' , sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
// connect
if (connect(sockfd , (struct sockaddr*)&peer , sizeof(peer)) < 0)
{
cerr << "connect error" << endl;
exit(3);
}
while(true)
{
request_t rq;
cout << "请输入左操作数#" ;
cin >> rq.left;
cout << "请输出右操作数#" ;
cin >> rq.right;
cout << "请输出操作符#" ;
cin >> rq.op;
write(sockfd , &rq , sizeof(rq));
response_t rp;
read(sockfd , &rp , sizeof(rp)) ;
cout << "code: " << rp.code << endl;
cout << "result:" << rp.result << endl;
}
return 0;
}
当服务端调用accept函数获取到新连接并创建新线程后 该线程就需要为该客户端提供计算服务 此时该线程需要先读取客户端发来的计算请求 然后进行对应的计算操作
void* Routine(void* arg)
{
pthread_detach(pthread_self()); //分离线程
int sock = *(int*)arg;
delete (int*)arg;
while (true){
request_t rq;
ssize_t size = recv(sock, &rq, sizeof(rq), 0);
if (size > 0){
response_t rp = { 0, 0 };
switch (rq.op){
case '+':
rp.result = rq.left + rq.right;
break;
case '-':
rp.result = rq.left - rq.right;
break;
case '*':
rp.result = rq.left * rq.right;
break;
case '/':
if (rq.right == 0){
rp.code = 1; //除0错误
}
else{
rp.result = rq.left / rq.right;
}
break;
case '%':
if (rq.right == 0){
rp.code = 2; //模0错误
}
else{
rp.result = rq.left % rq.right;
}
break;
default:
rp.code = 3; //非法运算
break;
}
send(sock, &rp, sizeof(rp), 0);
}
else if (size == 0){
cout << "service done" << endl;
break;
}
else{
cerr << "read error" << endl;
break;
}
}
close(sock);
return nullptr;
}
虽然当前代码存在很多潜在的问题 但这个代码能够很直观的告诉我们什么是约定 这里将其当作一份示意性代码
我们开始运行代码
如果是正常的计算 我们的计算器就能正常运行
如果涉及到除0 模0操作 该服务器就会返回我们一个错误码