目录
一、“协议” 的概念
二、结构化数据的传输
三、序列化和反序列化
序列化和反序列化的目的
四、网络版本计算器
服务端(server)
协议定制(protocal)
客户端(client)
服务器处理请求逻辑(Routine)
存在的问题('bug')
代码测试(test)
协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接,怎么互相识别。
为了使数据在网络上能够从源端口到目的端口,网络通信双方必须遵守相同的规则,将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来,只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
通信双方在进行网络通信时:
• 如果需要传输的数据时一个字符串,那么可以直接将这一字符串发送到网络中,此时对端也能从网络当中获取到这个字符串。
• 如果需要传输的是一些结构化的数据,此时就不能将这些数据一个一个发送到网络中了。
客户端最好把这些结构化的数据打包成一个整体后发送到网络中,服务器每次从网络中获取的数据就是一个完整的请求数据了。
比如实现一个网络版本计算器,需要客户端把要计算的两个数据,以及操作符发送过去,然后由服务器进行计算,最后将计算结果返回给客户端:
约定方案一:
• 客户端发送形如 ‘‘1+1’’的字符串。
• 这个字符串中有两个操作数,都是整型。
• 两个数字之间会有一个字符是运算符。
• 数字和运算符之间没有空格。
此时服务器再以相同方式对这个字符串进行解析,就可以从字符串中提取这些结构化数据。
约定方案二:
• 定制结构体来表示需要交换的信息。
• 发送数据时将这个结构体按照一个规则转换成网络标准数据格式,接受数据时再按照相同的规则把接受到的数据转化为结构体。
• 这个过程叫做 “序列化” 和 “反序列化”。
客户端可以定制一个结构体,将需要交互的信息定义到这个结构体中,客户端发送数据时先将数据进行序列化,服务端接收到数据化再对其进行反序列化,此时服务端就能得到客户端所发送过来的结构体了,再从结构体中提取出需要的数据。
• 序列化是将对象的状态信息转换为可以存储或者传输的形式(字节序列)的过程。
• 反序列化是把字节序列恢复为对象的过程。
• 在网络传输时,序列化的目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化之后都会变成二进制序列,此时底层在进行网络数据传输时看到的都是统一的二进制序列。
• 序列化或的二进制序列只有在网络传输时能被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
首先对服务器进行初始化:
• 调用 socket 函数,创建套接字
• 调用 bind 函数,对服务器进行绑定端口号等。
• 调用 listen 函数,将套接字设置成监听状态。
其次对服务器进行启动:
不断调用 accept 函数,从套接字中不断获取新连接,这里采用多线程版本,每当获取到一个新的连接后,就创建一个新线程,让新线程对客户端发来的数据进行计算服务。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define backlog 5
// ./server 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
cerr << "Usage: " << argv[0] << "port" << endl;
exit(1);
}
int port = atoi(argv[1]);
// 1、创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "listen_sock error!" << endl;
exit(2);
}
// 2、绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY); // 主机转网络
local.sin_port = htons(port);
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail!" << endl;
exit(3);
}
// 3、设置监听状态
if (listen(listen_sock, backlog) < 0)
{
cerr << "listen fail!" << endl;
exit(4);
}
// 4、启动服务器
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
for (;;)
{
socklen_t socklen = sizeof(peer);
//一直获取新连接
int sock = accept(listen_sock, (struct sockaddr *)&peer, &socklen);
if(sock < 0)
{
cerr<<"accept fail!"<
说明:
• 为了避免创建出来的套接字被下一次创建的套接字覆盖,采用在堆上开辟空间存储的形式存储该文件描述符。
数据可以分为请求数据和响应数据进行定制协议,采用结构体的方式来实现:
• 请求结构体:需要包含两个操作数,以及所所对应的操作符。
• 响应结构体:需要包含一个计算结果,和一个状态结果,用来标识本次计算的状态,因为可以本次计算出现异常(除0等)。
状态码规定:
• 状态码为0:表示计算成功,无异常现象。
• 状态码为1:表示出现除 0 异常。
• 状态码为2:表示出现模 0 异常。
• 状态码为3:表示其他非法计算,如输入错误的操作符等。
#pragma once
typedef struct request
{
int x;//左操作数
int y;//右操作数
char op;//操作符
} request;
typedef struct response
{
int code;//状态码
int result;//计算结果
} response;
首先对客户端进行初始化:
• 调用 socket 函数,创建套接字。
• 初始化完毕后,调用 connect 函数进行对服务器的连接,其次将请求发送给服务器。
• 客户端等待服务器处理完毕,发送回来结果后,还需要读取服务端的响应数据。
发送数据时:使用 write 或者 send 函数,这些函数的本质都是拷贝函数。
接收数据时:使用 read 或者 recv 函数,本质也是拷贝函数。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#include "protocal.h"
// ./client 127.0.0.1 8080
int main(int argc, char *argv[])
{
if (argc != 3)
{
cerr << "Usage: " << argv[0] << " server_ip server_port " << endl;
exit(1);
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "sock error!" << endl;
exit(2);
}
// 建立连接
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(server_port);
peer.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 网络转主机 inet_addr
if (connect(sock, (struct sockaddr *)&peer, sizeof(peer)) < 0)
{
cerr << "connect fail!" << endl;
exit(3);
}
// 发送请求
while (1)
{
request req;
cout << "请输入左操作数# ";
cin >> req.x;
cout << "请输入右操作数# ";
cin >> req.y;
cout << "请输入操作符[+-*/%]# ";
cin >> req.op;
send(sock, &req, sizeof(req), 0);
// 接受数据
response res;
recv(sock, &res, sizeof(res), 0); // 阻塞式
cout << "status: " << res.code << endl;
cout << req.x << req.op << req.y << " = " << res.result << endl;
}
return 0;
}
创建出来的新线程,需要对客户端发送到计算请求进行读取,然后进行计算操作,如果在计算过程中,出现除0等情况,只需要对 response 结构体填充进对应的状态码即可。
void *Rountine(void *arg)
{
// 线程分离,不需要再 wait
pthread_detach(pthread_self());
int sock = *(int *)arg;
delete (int *)arg;
while (1)
{
request req;
ssize_t n = recv(sock, &req, sizeof(req), 0);
if (n > 0)
{
// 进行计算任务
response res = {0, 0};
switch (req.op)
{
case '+':
res.result = req.x + req.y;
break;
case '-':
res.result = req.x - req.y;
break;
case '*':
res.result = req.x * req.y;
break;
case '/':
if (req.y == 0)
{
res.code = 1;
}
else
{
res.result = req.x / req.y;
}
break;
case '%':
if (req.y == 0)
{
res.code = 2;
}
else
{
res.result = req.x % req.y;
}
break;
default:
res.code = 3;
break;
}
// 将结果发送回客户端
send(sock, &res, sizeof(res), 0);
}
else if (n == 0)
{
// 对端停止发送数据,退出了
cout << "Client quit,me too!" << endl;
break;
}
else
{
cerr << "recv error!" << endl;
break;
}
}
close(sock);
return nullptr; // 返回结果
}
• 在发送和接收数据时没有进行对数据的序列化以及反序列化。
先运行服务器,./ server 8080 绑定端口号,再运行客户端 ./client 127.0.0.1 8080,然后进行发送数据,让服务器进行计算: