selph
selph
发布于 2022-02-14 / 326 阅读
0
0

TCPIP网络编程ch04-基于TCP的服务端客户端1

TCPIP协议栈

TCPIP协议栈有四层:

  • 链路层:是物理链接领域标准化的结果,专门定义WAN、LAN、MAN等网络标准,主机通过网络进行数据交换就需要这些物理连接
  • IP层:用于决定每次传输时的路径选择,IP本身是面向消息的不可靠的协议,不处理传输数据错误丢失
  • TCP/UDP层:完成实际数据传输,TCP协议可以保障可靠的数据
  • 应用层:根据程序特点决定客户端和服务端之间的传输规定

实现基于TCP的服务端和客户端

服务端

TCP服务器默认函数调用顺序:

image-20220212122402692

调用socket函数创建套接字,声明初始化地址信息结构体,调用bind函数分配地址


接下来调用listen函数:

int listen (int __fd, int __n);

参数1是进入接收请求状态的socket,参数2是请求连接队列长度

同时收到多个连接请求就会先放在队列里排队进行处理,请求队列长度根据实验结果而定


然后是受理客户端连接请求,调用accept函数:

int accept (int __fd,	// 服务器socket 
            __SOCKADDR_ARG __addr,	// 保存客户端地址
           socklen_t *__restrict __addr_len	// 第二个参数的长度
           );
// 成功时返回套接字文件描述符,失败返回-1

accept函数处理请求队列里的请求,会自动创建一个新的套接字进行与客户端的通信

客户端

客户端函数调用顺序:

image-20220212123125428

创建客户端套接字后向服务端请求连接,服务端调用listen函数之后创建连接等待队列,然后客户端即可请求连接,connect函数:

int connect(int sockfd, 				// 客户端socket
            const struct sockaddr *addr,// 服务端地址信息
            socklen_t addrlen			// 第二个参数的长度
           );
// 成功返回0,失败返回-1

connect函数在如下情况才会发生返回:

  • 服务端接收连接请求,指的是服务端把请求记录到等待队列
  • 发生断网等异常情况而中断连接请求

客户端的IP地址和端口号在调用connect函数时候自动分配,无需手动bind

客户端和服务端的调用关系

服务端创建套接字后调用bind,listen函数进入等待状态,客户端通过connect发起连接,服务端先于客户端的connect调用accept则会进入堵塞状态等待

image-20220212123551412

实现迭代echo服务端客户端

之前实现的服务端在处理一次请求之后就会退出,需要继续受理后续客户端请求,需要对代码进行扩展

插入循环语句反复调用accept函数即可

服务端代码

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

#define BUF_SIZE 1024

int main(int argc,char* argv[]){
    int serv_sock,clnt_sock;
    char message[BUF_SIZE];
    int len_str,i;

    sockaddr_in serv_addr,clnt_addr;
    socklen_t clnt_addr_len;

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

    // 1. 创建套接字
    serv_sock = socket(PF_INET,SOCK_STREAM,0);

    // 2. 填充服务端地址信息,绑定地址到套接字
    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. 设置套接字进入可监听状态
    if(!listen(serv_sock,5)){
        printf("Listening...\n");
    }

    // 4. 循环接收客户端请求并处理
    clnt_addr_len = sizeof(clnt_addr);
    while(true){
        // 接收请求
        clnt_sock = accept(serv_sock,(sockaddr*)&clnt_addr,&clnt_addr_len);
        if(clnt_sock == -1){
            printf("accept error\n");
            continue;
        }
        printf("recv a connect requist\n");

        // 处理单个请求,如果发送的
        while (true){
            memset(message,0,sizeof(message)-1);            
            int read_size = read(clnt_sock,message,sizeof(message)-1);
            //fputs(message,stdout);
            if(!strcmp(message,"Q\n") || !strcmp(message,"q\n")){
                write(clnt_sock,"interrupt the connection\n",26);
                break;
            }else{
                write(clnt_sock,message,read_size);
            }
        }
        printf("close the connection\n");
        close(clnt_sock);
    }

    // 5. 断开连接
    close(serv_sock);

    return 0;
}

客户端代码

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

#define BUF_SIZE 1024

int main(int argc,char* argv[]){
    int serv_sock;
    sockaddr_in serv_addr;

    if(argc !=3){
        printf("usage:%s <ip-address> <port>\n",argv[0]);
        return -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 = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    // 3. 连接服务端
    int ret = connect(serv_sock,(const sockaddr*)&serv_addr,sizeof(serv_addr));
    if(ret==-1){
        printf("connect failed!\n");
        return -1;
    }
    printf("connect success!\n");

    // 4. 数据交换
    int read_size = 0;
    while(true){
        char msg_write[BUF_SIZE]={0};
        char msg_read[BUF_SIZE]={0};
        
        // 输出提示信息 + 读取输入(注意,这里scanf不行)
        fputs("Input message(Q to quit) :",stdout);
        fgets(msg_write,sizeof(msg_write)-1,stdin);

        // 将获取到的输入发送给服务器
        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);
        
        // 如果发送了q,则断开连接
        if(!strcmp("q\n",msg_write) || !strcmp("Q\n",msg_write)){
            break;
        }
    }

    // 5. 断开连接
    close(serv_sock);    
    return 0;
}

运行效果

selph@selph:~/NetProgramStudy/ch4-linux$ ./echo_client 127.0.0.1 11114
connect success!
Input message(Q to quit) :asd
msg from server : asd
Input message(Q to quit) :asd
msg from server : asd
Input message(Q to quit) :ad
msg from server : ad
Input message(Q to quit) :q
msg from server : interrupt the connection

存在的问题

客户端代码这一段:

        // 将获取到的输入发送给服务器
        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);

每次调用read和write都会以字符串为单位执行实际IO操作,TCP不存在数据边界,多次调用write传递的字符串可能一次性到达服务器,可能客户端会一次性收到多个字符串,服务端可能一次write发不完数据会多次发

客户端也有可能在服务端write完成之前先read部分内容,读取不完整的字符串

这些问题来自于TCP数据传输特性,如何解决见下一章

这里没出现问题时因为运行环境太理想了

基于Windows实现

把Linux下的示例转换成Windows平台的,只要修改一下4点:

  • 通过WSAStartup,WSACleanup函数初始化并清除套接字相关库
  • 把数据类型和变量名切换成Windows风格(socket修改变量类型为SOCKET,SOCKADDR_IN大写)
  • 传输中用recv和send函数而非read和write
  • 关闭套接字用closesocket函数

服务端代码

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

#define BUF_SIZE 1024

int main(int argc, char* argv[]) {
    WSADATA wsaData = { 0 };
    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { // WSAStartup成功返回0
        exit(1);
    }

    SOCKET hServSock, hClntSock;
    char message[BUF_SIZE];
    int len_str, i;
    SOCKADDR_IN serv_addr, clnt_addr;
    int clnt_addr_len;

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

    // 1. 创建套接字
    hServSock = socket(PF_INET, SOCK_STREAM, 0);

    // 2. 填充服务端地址信息,绑定地址到套接字
    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(hServSock, (const sockaddr*)&serv_addr, sizeof(serv_addr));

    // 3. 设置套接字进入可监听状态
    if (!listen(hServSock, 5)) {
        printf("Listening...\n");
    }

    // 4. 循环接收客户端请求并处理
    clnt_addr_len = sizeof(clnt_addr);
    while (true) {
        // 接收请求
        hClntSock = accept(hServSock, (sockaddr*)&clnt_addr, &clnt_addr_len);
        if (hClntSock == -1) {
            printf("accept error\n");
            continue;
        }
        printf("recv a connect requist\n");

        // 处理单个请求,如果发送的
        while (true) {
            memset(message, 0, sizeof(message) - 1);
            int read_size = recv(hClntSock, message, sizeof(message) - 1,0);
            //fputs(message,stdout);
            if (!strcmp(message, "Q\n") || !strcmp(message, "q\n")) {
                send(hClntSock, "interrupt the connection\n", 26,0);
                break;
            }
            else {
                send(hClntSock, message, read_size,0);
            }
        }
        printf("close the connection\n");
        closesocket(hClntSock);
    }

    // 5. 断开连接
    closesocket(hServSock);
    WSACleanup();

    return 0;
}

客户端代码

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

#define BUF_SIZE 1024

int main(int argc, char* argv[]) {
    SOCKET hSock;
    SOCKADDR_IN serv_addr;

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

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

    // 1.创建socket    
    hSock = 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 = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    // 3. 连接服务端
    int ret = connect(hSock, (const sockaddr*)&serv_addr, sizeof(serv_addr));
    if (ret == -1) {
        printf("connect failed!\n");
        return -1;
    }
    printf("connect success!\n");

    // 4. 数据交换
    int read_size = 0;
    while (true) {
        char msg_write[BUF_SIZE] = { 0 };
        char msg_read[BUF_SIZE] = { 0 };

        // 输出提示信息 + 读取输入(注意,这里scanf不行)
        fputs("Input message(Q to quit) :", stdout);
        fgets(msg_write, sizeof(msg_write) - 1, stdin);

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

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

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

        // 如果发送了q,则断开连接
        if (!strcmp("q\n", msg_write) || !strcmp("Q\n", msg_write)) {
            break;
        }
    }

    // 5. 断开连接
    closesocket(hSock);
    WSACleanup();

    return 0;
}

运行效果

PS C:\Users\selph\source\Book\TCPIP\ch4-windows\x64\Debug> .\ch4-windows-client.exe 127.0.0.1 12345
connect success!
Input message(Q to quit) :hello selph
msg from server : hello selph
Input message(Q to quit) :q
msg from server : interrupt the connection

评论