selph
selph
发布于 2022-02-15 / 395 阅读
0
0

TCPIP网络编程ch05-基于TCP的服务端客户端2

echo客户端的完美实现

上一章里最后提到,实现的echo客户端存在问题:

        // 将获取到的输入发送给服务器
        write(serv_sock,msg_write,strlen(msg_write));

        // 从服务器那里读取数据
        read_size = read(serv_sock,msg_read,sizeof(msg_read)-1);
        msg_read[read_size] = 0;

        // 打印收到的信息
        printf("msg from server : %s",msg_read);

就是在真实网络中,服务端向客户端发送数据速度可能快也可能慢,因为这里代码里发送完数据立马读取数据,就有可能一次性读入了多条数据或者一次性读入了不完整的数据,导致出现功能性缺陷

本章开头给出了一个解决方案:

        // 将获取到的输入发送给服务器
        int write_size = 0; 
        int recv_size = 0;
        write_size = write(serv_sock,msg_write,strlen(msg_write));

        while(recv_size<write_size){
        	// 从服务器那里读取数据
        	read_size = read(serv_sock,msg_read,sizeof(msg_read)-1);
        	recv_size += read_size;
        }

        msg_read[read_size] = 0;
        // 打印收到的信息
        printf("msg from server : %s",msg_read);

这里服务器客户端传输的特点是客户端发送多少内容就会接收多少内容,所以这里判断是否收到了足够的内容再进行输出,通过循环来读取内容直到满足条件

如果不能提前接收到数据的长度,通过在收发过程中定好协议来表示数据边界,或者提前告知收发数据的大小

体验应用层协议:计算器服务器

这里是自己动手实现的,书上给出的源码是通过手动拼接字节数组来进行传输,这里使用结构体来传

协议设计:

  • 客户端将待运算数字个数,整数数组,运算符都装入结构体,计算结构体大小
  • 客户端连接到服务端之后先传输结构体大小
  • 服务端收到结构体大小之后返回“OK”
  • 客户端收到OK之后向服务端发送结构体
  • 服务端根据结构体内容进行计算
  • 服务器以4字节返回运算结果
  • 客户端得到结果就与服务端断开连接

这里定义了结构体来传输数据:

// 数据包
typedef struct _DATA{
    int op_num;     // 计算操作数数量
    char op_calc;   // 运算符类型:+ - * /
    int num_arr[0]; // 计算操作数数组
}DATA,*PDATA;

服务端-linux

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 数据包
typedef struct _DATA{
    int op_num;     // 计算操作数数量
    char op_calc;   // 运算符类型:+ - * /
    int num_arr[0]; // 计算操作数数组
}DATA,*PDATA;

// 四则计算处理函数
template<typename Op>
int calc(int const& op_num, int* const& num_arr){
    Op o;
    int i=0;
    int res = num_arr[i++];
    while(i<op_num) res = o(res,num_arr[i++]);
    return res;
}

int main(int argc,char* argv[]){
    PDATA pData; 
    int serv_sock;
    int clnt_sock;
    sockaddr_in serv_addr;
    sockaddr_in clnt_addr;
    socklen_t clnt_addr_len;

    // 验证参数数量
    if(argc != 2){
        printf("usage: %s <port>",argv[0]);
        exit(1);
    }

    // 1. 创建服务端socket
    serv_sock = socket(PF_INET,SOCK_STREAM,0);

    // 2. 绑定地址到服务端socket
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    bind(serv_sock,(const sockaddr*)&serv_addr,sizeof(serv_addr));

    // 3. 监听端口
    listen(serv_sock,5);
    printf("listen...\n");
    // 4. 接收请求
    clnt_addr_len = sizeof(clnt_addr);
    while (true){
        /* code */
        clnt_sock = accept(serv_sock,(sockaddr*)&clnt_addr,&clnt_addr_len);
        if(clnt_sock == -1){
            printf("connect failed!\n");
            continue;
        }else{
            printf("recv a connection\n");
        }
        // 5. 数据处理
        /*
        协议设计:
        - 客户端连接到服务端之后以1字节整数形式传递待运算数字个数
        - 客户端向服务端传递每个整数,4字节
        - 传递整数之后传递运算符信息(+ - * /),1字节
        - 服务器以4字节返回运算结果
        - 客户端得到结果就与服务端断开连接
        */

        // 接收结构体大小
        int size = 0;
        read(clnt_sock,&size,sizeof(int));
        if(size==0){
            printf("read info error\n");
            exit(-2);
        }
        write(clnt_sock,"OK",3);
        
        // 申请结构体空间 ,接收结构体
        pData = (PDATA)malloc(size);
        int read_size = 0;
        while(read_size < size){
            read_size += read(clnt_sock,pData+read_size,size);
        }

        // 处理运算数据
        int calc_res;
        switch (pData->op_calc)
        {
        case '+':
            calc_res = calc<std::plus<int>>(pData->op_num, pData->num_arr);break;
        case '-':
            calc_res = calc<std::minus<int>>(pData->op_num, pData->num_arr);break;
        case '*':
            calc_res = calc<std::multiplies<int>>(pData->op_num, pData->num_arr);break;
        case '/':
            calc_res = calc<std::divides<int>>(pData->op_num,pData->num_arr);break;
        default:
            calc_res = 0; break;
        }

        // 返回运算结果
        write(clnt_sock,&calc_res,sizeof(int));
        
        free(pData);
        printf("close connection\n");
        close(clnt_sock);
    }

    // 6. 关闭连接
    close(serv_sock);
    return 0;
}

客户端-Linux

#include <iostream>
#include <functional>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

typedef struct _DATA{
    int op_num;
    char op_calc;
    int num_arr[0];
}DATA,*PDATA;

int main(int argc,char* argv[]){
    int sock;
    sockaddr_in sock_addr;        
    PDATA pData;
    int pData_size=0;

    if(argc != 3){
        printf("usage: %s <ip> <port>\n",argv[0]);
        exit(-1);
    }

    // 1. 创建socket
    sock = socket(PF_INET,SOCK_STREAM,0);

    // 2. 连接服务端
    memset(&sock_addr,0,sizeof(sock_addr));
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_addr.s_addr = inet_addr(argv[1]);
    sock_addr.sin_port = htons(atoi(argv[2]));

    connect(sock,(const sockaddr*)&sock_addr,sizeof(sock_addr));

    // 3. 数据交互
    /*
        协议设计:
        - 客户端连接到服务端之后以1字节整数形式传递待运算数字个数
        - 客户端向服务端传递每个整数,4字节
        - 传递整数之后传递运算符信息(+ - * /),1字节
        - 服务器以4字节返回运算结果
        - 客户端得到结果就与服务端断开连接
    */

    // 获取运算数数量
    int op_num = 0;
    fputs("type op num:",stdout);
    op_num = fgetc(stdin) - '0';
    
    // 给通信结构体申请空间
    pData_size = sizeof(DATA)+op_num*sizeof(int);
    pData = (PDATA)malloc(pData_size);
    pData->op_num = op_num;

    // 依次输入操作数,保存到结构体里
    for (uint8_t i = 0; i < op_num; i++){
        printf("type the op %d: ",i);
        int buf=0;
        std::cin >> buf;
        pData->num_arr[i] = buf;
    }

    // 获取运算符,填充结构体
    char calc_op;
    std::cout << "type a calc type: ";
    std::cin >> calc_op;
    pData->op_calc = calc_op;

    // 发送结构体(先发大小,然后再发数据)
    write(sock,&pData_size,sizeof(pData_size));
    char readbuf[10];
    read(sock,readbuf,3);
    if(!strncmp(readbuf,"OK",2)){
        write(sock,pData,pData_size);
    }else{
        fputs("Server ERROR",stdout);
    }

    // 接收返回值
    int res;
    read(sock,&res,sizeof(res));
    std::cout << "res is "<< res<< std::endl;
    
    free(pData);
    close(sock);
    return 0;
}

运行结果

selph@selph:~/NetProgramStudy/ch5-linux$ ./op_client 127.0.0.1 12313
type op num:4
type the op 0: 100
type the op 1: 10
type the op 2: 1
type the op 3: 1000
type a calc type: +
res is 1111

TCP原理

补充讲解TCP部分理论

TCP套接字中的IO缓冲

write函数调用后不会立即传输数据,会把数据发送到输出缓冲区

read函数调用后不会立即接收数据,会把数据从输入缓冲区读取

在适当的时候(计算机自行进行判断选择),将输出缓冲区的内容传送到对方的输入缓冲区

IO缓冲特性可整理成:

  • IO缓冲在每个TCP套接字中单独存在
  • IO缓冲在创建套接字时自动生成
  • 即使关闭套接字也会继续传递输出缓冲区中的遗留内容
  • 关闭套接字将丢失输入缓冲区中的内容

TCP通过滑动窗口来控制传输的字节数,以确保不会因为缓冲溢出而丢失数据

TCP连接过程

TCP套接字建立连接的时候会进行三次握手:(套接字是全双工的工作方式)

主机A向主机B发起SYN包,表示要与主机B建立连接

主机B向主机A响应SYN+ACK包,表示收到建立请求,并且要与主机A建立连接

主机A向主机B发起ACK包,表示收到建立请求(回复主机B的SYN请求)

接下来就完成了连接建立,该进行数据交换了


数据交换的过程:

image-20220215223613684

主机A发送SEQ为1200的数据包,大小是100字节

主机B确认收到了,ACK表示接下来请发送SEQ1301的数据包

主机A发送SEQ为1301的包,大小100字节

主机B确认收到后,ACK响应以获取接下来的包


若发生超时或丢包,没有收到ACK相应,则会将上一个传递的数据包再次传输:

image-20220215223837420


断开连接也需要进行双方协商:

image-20220215224204355

过程是连接双方分别向对方发送FIN数据包,请求断开连接,当对方准备好断开之后响应ACK数据包

两边都FIN-ACK结束后(四次挥手),连接断开

基于Windows的实现

从Linux迁移到Windows基本上没啥需要改动的,直接复制黏贴,然后改一下write和read为send和recv,close为closesocket,然后加上WSAStartup初始化和WSACleanup清理即可完成迁移

到目前为止,Windows和Linux还是很好切换的

服务端-Windows

#include <iostream>
#include <functional>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

// 数据包
typedef struct _DATA {
    int op_num;     // 计算操作数数量
    char op_calc;   // 运算符类型:+ - * /
    int num_arr[0]; // 计算操作数数组
}DATA, * PDATA;

// 四则计算处理函数
template<typename Op>
int calc(int const& op_num, int* const& num_arr) {
    Op o;
    int i = 0;
    int res = num_arr[i++];
    while (i < op_num) res = o(res, num_arr[i++]);
    return res;
}

int main(int argc, char* argv[]) {
    PDATA pData;
    SOCKET serv_sock;
    SOCKET clnt_sock;
    sockaddr_in serv_addr;
    sockaddr_in clnt_addr;
    int clnt_addr_len;

    WSADATA wsaData = { 0 };
    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { // WSAStartup成功返回0
        exit(1);
    }

    // 验证参数数量
    if (argc != 2) {
        printf("usage: %s <port>", argv[0]);
        exit(1);
    }

    // 1. 创建服务端socket
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    // 2. 绑定地址到服务端socket
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    bind(serv_sock, (const sockaddr*)&serv_addr, sizeof(serv_addr));

    // 3. 监听端口
    listen(serv_sock, 5);
    printf("listen...\n");
    // 4. 接收请求
    clnt_addr_len = sizeof(clnt_addr);
    while (true) {
        /* code */
        clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_len);
        if (clnt_sock == -1) {
            printf("connect failed!\n");
            continue;
        }
        else {
            printf("recv a connection\n");
        }
        // 5. 数据处理
        /*
        协议设计:
        - 客户端连接到服务端之后以1字节整数形式传递待运算数字个数
        - 客户端向服务端传递每个整数,4字节
        - 传递整数之后传递运算符信息(+ - * /),1字节
        - 服务器以4字节返回运算结果
        - 客户端得到结果就与服务端断开连接
        */

        // 接收结构体大小
        int size = 0;
        recv(clnt_sock, (char*) &size, sizeof(int), 0);
        if (size == 0) {
            printf("read info error\n");
            exit(-2);
        }
        send(clnt_sock, "OK", 3, 0);

        // 申请结构体空间 ,接收结构体
        pData = (PDATA)malloc(size);
        int read_size = 0;
        while (read_size < size) {
            read_size += recv(clnt_sock, (char*)(pData + read_size), size, 0);
        }

        // 处理运算数据
        int calc_res;
        switch (pData->op_calc)
        {
        case '+':
            calc_res = calc<std::plus<int>>(pData->op_num, pData->num_arr); break;
        case '-':
            calc_res = calc<std::minus<int>>(pData->op_num, pData->num_arr); break;
        case '*':
            calc_res = calc<std::multiplies<int>>(pData->op_num, pData->num_arr); break;
        case '/':
            calc_res = calc<std::divides<int>>(pData->op_num, pData->num_arr); break;
        default:
            calc_res = 0; break;
        }

        // 返回运算结果
        send(clnt_sock, (const char*)&calc_res, sizeof(int), 0);

        free(pData);
        printf("close connection\n");
        closesocket(clnt_sock);
    }

    // 6. 关闭连接
    closesocket(serv_sock);
    WSACleanup();
    return 0;
}

客户端-Windows

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <functional>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

typedef struct _DATA {
    int op_num;
    char op_calc;
    int num_arr[0];
}DATA, * PDATA;

int main(int argc, char* argv[]) {
    SOCKET sock;
    sockaddr_in sock_addr;
    PDATA pData;
    int pData_size = 0;

    WSADATA wsaData = { 0 };
    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { // WSAStartup成功返回0
        exit(1);
    }

    if (argc != 3) {
        printf("usage: %s <ip> <port>\n", argv[0]);
        exit(-1);
    }

    // 1. 创建socket
    sock = socket(PF_INET, SOCK_STREAM, 0);

    // 2. 连接服务端
    memset(&sock_addr, 0, sizeof(sock_addr));
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_addr.s_addr = inet_addr(argv[1]);
    sock_addr.sin_port = htons(atoi(argv[2]));

    connect(sock, (const sockaddr*)&sock_addr, sizeof(sock_addr));

    // 3. 数据交互
    /*
        协议设计:
        - 客户端连接到服务端之后以1字节整数形式传递待运算数字个数
        - 客户端向服务端传递每个整数,4字节
        - 传递整数之后传递运算符信息(+ - * /),1字节
        - 服务器以4字节返回运算结果
        - 客户端得到结果就与服务端断开连接
    */

    // 获取运算数数量
    int op_num = 0;
    fputs("type op num:", stdout);
    op_num = fgetc(stdin) - '0';

    // 给通信结构体申请空间
    pData_size = sizeof(DATA) + op_num * sizeof(int);
    pData = (PDATA)malloc(pData_size);
    pData->op_num = op_num;

    // 依次输入操作数,保存到结构体里
    for (uint8_t i = 0; i < op_num; i++) {
        printf("type the op %d: ", i);
        int buf = 0;
        std::cin >> buf;
        pData->num_arr[i] = buf;
    }

    // 获取运算符,填充结构体
    char calc_op;
    std::cout << "type a calc type: ";
    std::cin >> calc_op;
    pData->op_calc = calc_op;

    // 发送结构体(先发大小,然后再发数据)
    send(sock, (const char*)&pData_size, sizeof(pData_size), 0);
    char readbuf[10];
    recv(sock, readbuf, 3,0);
    if (!strncmp(readbuf, "OK", 2)) {
        send(sock, (const char*)pData, pData_size,0);
    }
    else {
        fputs("Server ERROR", stdout);
    }

    // 接收返回值
    int res;
    recv(sock, (char*)&res, sizeof(res), 0);
    std::cout << "res is " << res << std::endl;

    free(pData);
    closesocket(sock);

    return 0;
}

运行结果

PS C:\Users\selph\source\Book\TCPIP\ch5-windows\x64\Debug> .\ch5-client.exe 127.0.0.1 11112
type op num:5
type the op 0: 1
type the op 1: 2
type the op 2: 3
type the op 3: 4
type the op 4: 5
type a calc type: *
res is 120

评论