【Linux网络编程】- 认识 ‘’协议‘‘ | 网络版本计算器


目录

一、“协议” 的概念

二、结构化数据的传输

三、序列化和反序列化

序列化和反序列化的目的

 四、网络版本计算器

服务端(server)

协议定制(protocal)

客户端(client)

服务器处理请求逻辑(Routine)

存在的问题('bug')

代码测试(test)


一、“协议” 的概念

        协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接,怎么互相识别。

        为了使数据在网络上能够从源端口到目的端口,网络通信双方必须遵守相同的规则,将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来,只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。


二、结构化数据的传输

通信双方在进行网络通信时:

• 如果需要传输的数据时一个字符串,那么可以直接将这一字符串发送到网络中,此时对端也能从网络当中获取到这个字符串。

• 如果需要传输的是一些结构化的数据,此时就不能将这些数据一个一个发送到网络中了。

客户端最好把这些结构化的数据打包成一个整体后发送到网络中,服务器每次从网络中获取的数据就是一个完整的请求数据了。

比如实现一个网络版本计算器,需要客户端把要计算的两个数据,以及操作符发送过去,然后由服务器进行计算,最后将计算结果返回给客户端:

约定方案一:

• 客户端发送形如 ‘‘1+1’’的字符串。

• 这个字符串中有两个操作数,都是整型。

• 两个数字之间会有一个字符是运算符。

• 数字和运算符之间没有空格。

此时服务器再以相同方式对这个字符串进行解析,就可以从字符串中提取这些结构化数据。

约定方案二:

• 定制结构体来表示需要交换的信息。

• 发送数据时将这个结构体按照一个规则转换成网络标准数据格式,接受数据时再按照相同的规则把接受到的数据转化为结构体。

• 这个过程叫做 “序列化” 和 “反序列化”。

客户端可以定制一个结构体,将需要交互的信息定义到这个结构体中,客户端发送数据时先将数据进行序列化,服务端接收到数据化再对其进行反序列化,此时服务端就能得到客户端所发送过来的结构体了,再从结构体中提取出需要的数据。


三、序列化和反序列化

• 序列化是将对象的状态信息转换为可以存储或者传输的形式(字节序列)的过程。

• 反序列化是把字节序列恢复为对象的过程。

序列化和反序列化的目的

•  在网络传输时,序列化的目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化之后都会变成二进制序列,此时底层在进行网络数据传输时看到的都是统一的二进制序列。

•  序列化或的二进制序列只有在网络传输时能被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

【Linux网络编程】- 认识 ‘’协议‘‘ | 网络版本计算器_第1张图片


 四、网络版本计算器

服务端(server)

首先对服务器进行初始化:

• 调用 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!"<

说明:

• 为了避免创建出来的套接字被下一次创建的套接字覆盖,采用在堆上开辟空间存储的形式存储该文件描述符。

协议定制(protocal)

数据可以分为请求数据和响应数据进行定制协议,采用结构体的方式来实现:

• 请求结构体:需要包含两个操作数,以及所所对应的操作符。

• 响应结构体:需要包含一个计算结果,和一个状态结果,用来标识本次计算的状态,因为可以本次计算出现异常(除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;

客户端(client)

首先对客户端进行初始化:

• 调用 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;
}

服务器处理请求逻辑(Routine)

创建出来的新线程,需要对客户端发送到计算请求进行读取,然后进行计算操作,如果在计算过程中,出现除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; // 返回结果
}

存在的问题('bug')

• 在发送和接收数据时没有进行对数据的序列化以及反序列化。

代码测试(test)

先运行服务器,./ server 8080 绑定端口号,再运行客户端 ./client 127.0.0.1 8080,然后进行发送数据,让服务器进行计算:

【Linux网络编程】- 认识 ‘’协议‘‘ | 网络版本计算器_第2张图片


你可能感兴趣的:(【Linux网络编程】,网络)