Linux高级编程——TCP(套接字)(双向通信、文件传输)

网络通信编程实现

基于 TCP 协议的通信

TCP:Transmission Control Protocol,传输控制协议,传输层的一个非常重要的协议,负责数据传送。面向连接、传输可靠、但通信效率较低。

基于 TCP 通常采用 C/S 模式,即客户端(Client)/服务器(Server)模式,客户端程序和服务器端程序的实现不一样,它们是不对等的。每次通信都是由客户端主动发起(即连接服务器),服务器端被动参与通信(即接收客户端的连接请求)。

TCP通信理论原理
TCP首部

Linux高级编程——TCP(套接字)(双向通信、文件传输)_第1张图片

网络传输的数据包由两部分组成:一是协议所要用到的首部,另一部分是上层传过来的数据。TCP 的首部承载着 TCP 协议协议的各项信息:

  • TCP端口号(源 IP +源端口号)+(目的 IP + 目的端口号)

TCP 首部预留了两个16位给端口号存储,也就是端口号的范围是2^16 = 65535,前1024以下给系统保留,其他的作为用户使用的端口范围;

  • TCP的序号和确认号

32位序号 seq:(Sequence number),TCP 通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个序号来确认发送数据的顺序。

32位确认号 ack:(Acknowledge number),TCP 对上一次的seq序号做出的确认号,用来响应 TCP 报文段,给收到的 TCP 报文段的序号seq+1;

  • TCP的标志位
  1. SYN:同步标志位,用于建立会话连接,同步序列号;
  2. ACK:确认标志位,对已接受的数据包进行确认;
  3. FIN:完成标志位,表示我已经没有数据要发送,即将关闭连接;
  4. PSH:推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队。
  5. RST:重置标志位,用于连接复位、拒绝错误和非法的数据包;
  6. URG:紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;
TCP三次握手建立连接

在 socket 编程中,客户端执行 connect()完成;
Linux高级编程——TCP(套接字)(双向通信、文件传输)_第2张图片

  • **第一次握手:**客户端将 TCP 报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号字段里,指明客户端打算连接的服务器的端口,并将数据包发送给服务器端,发送完毕后,客户端进入(SYN_SENT)等待状态;

  • **第二次握手:**服务器端收到数据包后由标志位SYN = 1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号seq = K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态;

  • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack = K+1,并将该数据包发送给服务器端,服务器端检查是否为K+1,ACK是否为1如果正确则连接建立成功,客户端和服务器端进入 ESTABLISHED 状态,完成三次握手,随后客户端与服务器端之间就可以开始传输数据了;

TCP四次挥手关闭连接

通过调用close()函数完成

Linux高级编程——TCP(套接字)(双向通信、文件传输)_第3张图片

  • 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
  • 第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
  • 第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
  • 第四次挥手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。

基于 TCP 协议通信的过程如同打电话的通信过程。

TCP 服务器端实现步骤:
  1. 创建一个(监听)套接字(Socket),相当于买一部手机。调用 socket 函数实现,不用花钱!
  2. 给套接字绑定地址(IP + Port),相当于给手机绑定一个手机号。调用 bind 函数,不用花钱!
  3. 将套接字设置为监听状态,相当于将手机设置为待机状态。调用listen 函数实现。
  4. 接受客户端连接请求,相当于接受其他人的来电请求。调用accept 函数实现。
  5. 收发数据,相当于互相通话。调用 send/writerecv/read 函数实现。
  6. 断开客户端连接,相当于通话完毕挂机。调用 close 函数实现。
  7. (上面三步可以重复进行,如果不想继续通信)关闭(监听)套接字,相当于销毁手机。调用 close 函数实现。

套接字(Socket):网络接入点,即进程必须通过套接字接入计算机网络中才能进行通信。

TCP 客户端实现步骤:
  1. 创建一个套接字,相当于买一部手机,调用socket 函数实现,不用花钱!
  2. (可选)给套接字绑定地址(IP + Port),相当于给手机绑定一个手机号。调用 bind 函数实现,可以选择一个很酷的端口号,不用花钱!(显式绑定)

在第三步调用connect函数时,connect 函数内部会检测套接字是否已经成功绑定地址,如果没有显式绑定或绑定失败了,会随机选择一个空闲端口号和本机任意 IP 跟套接字绑定起来(隐式绑定

  1. 连接服务器,相当于拨打别人电话。调用 connect 函数实现。
  2. 收发数据,相当于互相通话。调用 send/write,recv/read 函数实现。
  3. 断开连接,相当于挂机。调用 close 函数实现
一次通信实例
简易实例:服务器端{发一句,收一句}

#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
    //socket 函数:创建一个新套接字
    //参数意义:
    //第一个参数:地址家族,通常是 AF_INET 族
    //第二个参数:套接字类型,通常有:SOCK_STREAM(流套接字,用于 TCP 协议通信)和 SOCK_DGRAM
    //第三个参数:通常为 0,表示使用默认协议
    //返回值为新套接字的文件描述符,如果失败则为 -1
    
    //第一步:创建一个新的监听套接字
    int sock_listen = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 ==sock_listen)
    {
        perror("socket");
        return 1;
    }

    //第二步:绑定地址
    //指定地址信息
    struct sockaddr_in myaddr;
    myaddr.sin_family = AF_INET;            //指定地址家族为 Internet 地址家族
    myaddr.sin_addr.s_addr = INADDR_ANY;    //指定 IP 地址为本机任意地址
    //myaddr.sin_addr.s_addr = inet_addr("192.162.0.32");     //指定 IP 地址为本机某个确定 IP
    myaddr.sin_port = htons(8888);          //指定端口号为 8888 (D)——22B8(H)小端——B822(H)大端——47138(D)
    //printf("%d\n",htons(8888));
    
    //htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
    //inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序。

    //将上面指定的地址和套接字绑定
    if(bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
    {
        perror("bind");
        return 1;
    }

    //第三步:监听
    //listen 函数的第二个参数为监听等待队列的长度
    if(-1 == listen(sock_listen, 5))
    {
        perror("listen");
        return 1;
    }

    //第四步:接受客户端的请求
    //accept(sock_listen, NULL, NULL);//如果对客户端地址信息不感兴趣
    
    //如果想获取当前客户端的地址信息,就使用下面的方式:
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    
    //调用 accept 函数接收一个客户端连接请求(队头请求)
    //如果成功,返回值为一个套接字文件描述符,这个套接字是和该客户端一一对应的
    //这个套接字专门用于和对应的客户端通信,所以通常称它为连接套接字。

    //accpte
    int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len); //第二个为结构体的指针,第三个参数为结构体的长度
    
    if(-1 ==sock_conn)
    {
        perror("accept");
    }

    //第五步:发送数据
    char msg[100] = "我是服务器!";
    double d = 3.14;

    //send(sock_conn, msg, sizeof(msg), 0);//最后一个参数为发送标准,默认为 0
    //send(sock_conn, &d, sizeof(d), 0);
    int ret = write(sock_conn, msg, sizeof(msg));
    //write(sock_conn, &d, sizeof(d));

    if(ret == sizeof(msg))
    {
        printf("发送成功!\n");
    }

    //接收数据,如果当时没有任何数据,recv 函数会阻塞当前线程,直到成功收到数据或对方断开连接或出错。
    ret = recv(sock_conn, msg, sizeof(msg), 0);   //同理用 read 等效
    //recv(sock_conn, &d, sizeof(d), 0);
    
    if(ret > 0)
    {
        printf("客户端说:%s\n",msg);
    }

    //第六步:断开客户端连接
    close(sock_conn);

    //第七步:关闭监听套接字
    close(sock_listen);
    

    return 0;
}
简易实例:客户端{收一句,发一句}

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main(int argc, char** argv)
{
    //socket 函数:创建一个新套接字
    // 参数意义:
    // 第一个参数:地址家族,通常是 AF_INET
    // 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
    // 第三个参数:通常为 0, 表示使用默认协议。
    // 返回值为新套接字的文件描述符,如果失败则为 -1

    // 第一步:创建一个新的套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 == sock)
    {
        perror("socket");
        return 1;
    }

    // // 第二步:绑定地址

    // // 指定地址信息
    // struct sockaddr_in myaddr;
    // myaddr.sin_family = AF_INET;            // 指定地址家族为 Internet 地址家族
    // myaddr.sin_addr.s_addr = INADDR_ANY;    // 指定 IP 地址为本机任意地址
    // //myaddr.sin_addr.s_addr = inet_addr("192.168.0.56");    // 指定 IP 地址为本机某个确定的 IP 地址
    // myaddr.sin_port = htons(8888);          // 指定端口号为 8888
    // // htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
    // // inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序

    // // 将上面指定的地址和套接字绑定
    // if(-1 == bind(sock, (struct sockaddr*)&myaddr, sizeof(myaddr)))
    // {
    //     perror("bind");
    //     return 1;
    // }

    // 第三步:连接服务器

    // 指定服务器的地址
    struct sockaddr_in srv_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    srv_addr.sin_port = htons(atoi(argv[2]));

    if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
    {
        perror("connect");
        return 1;
    }

    // 第四步:收发数据

    // 发送数据
    char msg[100];
    int ret;

    // // 接收数据,如果当前没有任何数据,recv 函数会阻塞当前线程,直到成功接收到数据或对方端开连接(返回 0)或出错(返回 -1)。
    ret = recv(sock, msg, sizeof(msg), 0);
    // // read(sock_conn, msg, sizeof(msg));  // 和上面的写法等效

    if(ret > 0)
    {
        printf("服务器端说:%s\n", msg);
    }

    strcpy(msg, "你是傻子!");
    ret = send(sock, msg, sizeof(msg), 0);
    //write(sock_conn, msg, sizeof(msg));  // 和上面的写法等效

    if(ret == sizeof(msg))
    {
        printf("发送成功!\n");
    }

    // 第五步:端开连接
    close(sock);
    
    return 0;
}

文件传输实例
//服务器端:循环等待发送数据 ./执行文件  文件(不能是文件夹)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main(int argc, char** argv)
{
    //socket 函数:创建一个新套接字
    int sock_listen = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 == sock_listen)
    {
        perror("socket");
        return 1;
    }

    // 第二步:绑定地址
    // 指定地址信息
    struct sockaddr_in myaddr;
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = INADDR_ANY;

    myaddr.sin_port = htons(8888); 
 
    // 将上面指定的地址和套接字绑定
    if(-1 == bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
    {
        perror("bind");
        return 1;
    }

    // 第三步:监听
    if(-1 == listen(sock_listen, 5))
    {
        perror("listen");
        return 1;
    }

    while(1)
    {
        // 第四步:接受客户端连接请求

        //accept(sock_listen, NULL, NULL);  // 如果对客户端地址信息不感兴趣

        // 如果想获取当前客户端的地址信息,就使用下面的方式
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);

       
        int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len);

        if(-1 == sock_conn)
        {
            perror("accept");
            continue;
        }

        printf("\n%s:%hu 已连接...\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 第五步:收发数据

        // 发送数据
        char file_name[300];
        char msg[1024];
        int ret;
        unsigned long cnt1 = 0, cnt2 = 0;
        time_t start, end;
        char* p = NULL;

        p = strrchr(argv[1], '/');

        if(NULL == p)
        {
            strcpy(file_name, argv[1]);
        }
        else
        {
            strcpy(file_name, p + 1);
        }

        send(sock_conn, file_name, sizeof(file_name), 0);

        int fd = open(argv[1], O_RDONLY);

        printf("正在努力发送文件...\n");

        time(&start);

        while((ret = read(fd, msg, sizeof(msg))) > 0)
        {
            cnt1 += ret;

            ret = send(sock_conn, msg, ret, 0);
            //write(sock_conn, msg, sizeof(msg));  // 和上面的写法等效

            if(ret == -1) 
            {
                printf("发送文件失败!\n");
                break;
            }

            cnt2 += ret;
        }

        if(cnt1 != 0 && cnt1 == cnt2)
        {
            time(&end);
            printf("发送文件成功!(耗时 %d 秒,平均网速 %d B/s)\n", end - start, cnt1 / (end -start));
        }

        // 第六步:断开客户端连接
        close(sock_conn);
    }

    // 第七步:关闭监听套接字
    close(sock_listen);
    
    return 0;
}

// 客户端 输入 ./执行文件 IP Port
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 



int main(int argc, char** argv)
{
    // 第一步:创建一个新的套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 == sock)
    {
        perror("socket");
        return 1;
    }


    // 第三步:连接服务器

    struct sockaddr_in srv_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    srv_addr.sin_port = htons(atoi(argv[2]));

    if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
    {
        perror("connect");
        return 1;
    }

    // 第四步:收发数据

    // 发送数据
    char msg[300];
    int ret;
    ret= recv(sock,msg,sizeof(msg), 0);

    int fd = open("msg",O_WRONLY | O_CREAT);
    
    while(1)
    {
        ret = recv(sock, msg, sizeof(msg), 0);
        
        if(ret <= 0) break; //0为正常断开,-1为异常断开
        write(fd,msg,ret);
    }

    close(fd);
    // 第五步:端开连接
    close(sock);
    
    return 0;
}

你可能感兴趣的:(Linux高级编程,linux,网络,服务器)