6-基于TCP的客户端/服务端通信

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82287590

1. 客户端/服务端通信流程

  在前面的学习中,我们学习了客户端和服务端进行网络通信的函数接口,下面这张图就是客户端和服务端之间的通信流程,相信你应该不陌生了。

这里写图片描述
图1-客户端/服务器通信流程


2. 示例

服务器程序
  程序实现服务端从客户端读字符,然后将每个字符转换为大写并回写给客户端。

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

#define SERV_PORT 10001
#define SERV_IP "192.168.0.107"

int main(void) {
    int sfd, cfd;
    int len, i;
    //BUFSIZ是系统内嵌的一个宏,用来指定buf大小
    char buf[BUFSIZ], clie_IP[BUFSIZ];
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&serv_addr, sizeof(serv_addr));      
    serv_addr.sin_family = AF_INET;           
    inet_pton(AF_INET , SERV_IP , &serv_addr.sin_addr.s_addr);
    serv_addr.sin_port = htons(SERV_PORT);              

    //绑定套接字
    bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    //设定连接上限,这里不会阻塞
    listen(sfd, 64);
    printf("wait for client connect ...\n");
    clie_addr_len = sizeof(clie_addr);
    //阻塞等待客户端发起连接
    cfd = accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
    //打印客户端的ip地址和端口号
    printf("client IP:%s\tport:%d\n", 
            inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
            ntohs(clie_addr.sin_port));
    //循环处理客户端的数据请求
    while (1) {
        len = read(cfd, buf, sizeof(buf));
        //read返回0说明对端已经关闭
        if(len == 0){
            break;
        }
        write(STDOUT_FILENO, buf, len);

        //处理客户端数据,小写转大写
        for (i = 0; i < len; i++){
            buf[i] = toupper(buf[i]);
        }
        //处理完数据,回写给客户端
        write(cfd, buf, len);
    }
    //关闭连接
    close(sfd);
    close(cfd);
    return 0;
}



客户端程序
客户端程序从控制台中获得一个字符串发给服务器,然后接收服务器转换后大写的字符串并打印到控制台。

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

#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001

int main(void) {
    int sfd, len;
    struct sockaddr_in serv_addr;
    char buf[BUFSIZ];
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&serv_addr, sizeof(serv_addr));                       
    serv_addr.sin_family = AF_INET;                         
    inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); 
    serv_addr.sin_port = htons(SERV_PORT);                      
    connect(sfd, (struct sockaddr *)&serv_addr ,  sizeof(serv_addr));
    //循环读写数据
    while (1) {
        fgets(buf, sizeof(buf), stdin);
        //将数据写给服务器
        write(sfd, buf, strlen(buf)); 
        //从服务器读取转换后数据
        len = read(sfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);

        if(buf[0] == 'Q'){
            break;
        }
}
    //关闭链接
    close(sfd);
    return 0;
}



程序执行结果:

这里写图片描述

  如上图所示,客户端向服务端发送一个hello world字符串,然后服务器将字符串转大写后再发回到客户端,接着客户端再输入q关闭客户端和服务器之间的tcp连接。

  这里我们思考一个问题,客户端和服务端进行通信时,tcp协议做了哪些事情,在网络中又是怎么进行的?接下来我们就从网络的角度来分析客户端和服务端的通信流程。


3. 基于tcp的客户端/服务端通信分析

这里写图片描述
图2-通信过程

  图2中展示了客户端(192.168.0.101)和服务端(192.168.0.107)之间的整个通信流程在网络中是怎么进行的(有兴趣的小伙伴可以自己做实验分析数据包或者参考tcp/ip协议学习笔记目录,这里就略过不讲了),而根据上图我们大致可以把基于tcp协议的客户端和服务端通信流程大致分为以下三个过程:即建立tcp连接过程,数据传输过程,关闭tcp连接过程,下面我们将会围绕这三个过程进行详细的分析。


3.1 建立连接过程

这里写图片描述
图3-建立连接过程

  首先是建立连接过程,服务端会调用socket()、bind()、listen()函数完成初始化等一系列准备工作后,再调用accept()阻塞等待客户端的连接。

  而客户端调用socket()创建套接字后,就可以调用connect()向服务端发出SYN段并阻塞等待确认,当服务器收到对端的SYN段时,就会回复一个SYN + ACK段(SYN+ACK段有两个作用,一是服务端对客户端的SYN段的确认,二是服务端向客户端发起建立连接)。

  当客户端收到SYN+ACK段后就会从connect()返回,并给服务器发送一个ACK段,服务器收到这个ACK段后,此时客户端和服务端之间已经建立tcp连接,就会从accept()返回,开始处理客户端的数据请求。


3.2 数据传输过程

这里写图片描述
图4-数据传输的过程

  当客户端和服务端建立tcp连接后,按照tcp通信流程,服务器从accept()返回后会分配一个新的cfd跟客户端通信,并立刻调用read(cfd)读取客户端的请求数据,如果客户端还没有发送数据,服务端则会在read处阻塞等待。

  当客户端和服务端建立tcp连接后,按照tcp通信流程,服务器从accept()返回后会分配一个新的cfd跟客户端通信,并立刻调用read(cfd)读取客户端的请求数据,如果客户端还没有发送数据,服务端则会在read处阻塞等待。

  这时客户端调用write()向服务器发送了一个hello world字符串,服务器收到这个字符串后从read()返回,对客户端的字符串进行小写转大写处理,在此期间客户端调用read()阻塞等待服务器的回写数据,服务器调用write()将转换后的大写字符串再发回给客户端,并再次调用read()阻塞等待下一条请求,当客户端收到后从read()返回,调用write()继续发送数据,也就是说客户端和服务端的数据传输过程是可以循环的。


3.3 关闭连接过程

这里写图片描述
图5-关闭连接过程

  如果客户端没有数据要发送的话,就可以调用close()来关闭tcp连接,当对端一关闭,那么服务器调用read()就会返回0(其实read返回0也有可能是读到文件尾,返回EOF了),这样服务器就知道客户端关闭了连接,也会调用close()关闭连接。为什么要关闭两个方向的连接?这其实跟网络通信方式有关,由于tcp协议是采用全双工通信方式的,在关闭连接时,需要把客户端和服务器两个方向的连接都关闭掉。


4. 网络通信方式

这里写图片描述
图6-通信方式

  实际上客户端和服务端进行通信时,系统内核会为客户端和服务端各自维护一个套接字,服务端的套接字sfd是由accept()返回的新的套接字,而客户端的套接字是socket()创建的套接字,每个套接字中都有一个读缓冲区和写缓冲区,分别对应读端和写端,也就是说双方在通信时,同一时刻又能读又能写,这就是双向全双工通信方式,但是这些事情我们并不需要管,因为内核已经帮我们做好了,所以我们在用户层基本感觉不到还有两个缓冲区的。

  有同学可能会问,那我们在程序中定义的buf跟套接字中的缓冲区是一样的吗?显然是不一样的,套接字中的缓冲区是内核层面的,而程序中定义的buf是用户层的,注意区分。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82287590