ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

【Linux】网络编程套接字(二)—— TCP编程

2020-12-26 21:33:01  阅读:149  来源: 互联网

标签:return addr 编程 TCP ret sockfd 接字 buf 连接


文章目录

1. TCP协议通信流程

在这里插入图片描述

1.1 初始化

服务器:

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来。

客户端:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求。

1.2 建立连接 —— 三次握手

首先客户端和服务端都是关闭状态,也就是CLOSED状态。服务器端进入一个监听的LISTEN状态,阻塞等待客户端的连接。

  • 第一次握手客户端向服务端发送一个SYN的标志位以请求连接。此时客户端进入SYN_SEND状态,也就是开始阻塞等待服务器的应答。
  • 第二次握手服务端收到了客户端的SYN连接请求,也就处于SYN_RCVD状态。由于现在客户端向服务端单方面请求连接了,要想双方都建立连接的话,就需要服务端也向客户端发送一个SYN请求。同时,服务端还需要响应给客户端一个ACK应答,告诉客户端接收到了你的连接请求并同意建立连接
  • 第三次握手客户端接收到服务端的ACK应答和SYN连接之后,说明连接已经成功的建立,所以客户端处于ESTABLISHED状态。但是此时还需要返回给服务端一个ACK应答,告诉服务器我收到了你的连接请求。当服务端接收到最后一个ACK应答之后,就说明双方已经建成了连接通信,服务端也处于一个EXTABLISHED状态。

流程如图所示
在这里插入图片描述
这个建立连接的过程, 通常称为三次握手

1.3 数据传输

  • 建立连接后, TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。

  • 服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。

  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答。

  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。

  • 客户端收到后从read()返回, 发送下一条请求, 如此循环下去。

1.4 关闭连接 —— 四次挥手

在数据传输之后,由主动断开连接方来进行请求关闭连接。

  • 第一次挥手主动断开连接方调用close()方法关闭连接,向被动断开连接方发送一个FIN关闭连接请求,此时主动断开连接方就处于FIN_WAIT1状态。
  • 第二次挥手被动断开连接方收到主动断开连接方的FIN之后,处于CLOSE_WAIT状态,说明被动断开连接方要准备关闭连接了。同时会先返回一个ACK应答,告诉主动断开连接方,我收到了你的关闭连接请求。此时收到ACK应答的主动断开连接方处于一个FIN_WAIT2状态,也就是继续等待的状态。
  • 第三次挥手被动断开连接方调用close()方法关闭连接,发送给主动断开连接方一个FIN结束报文段。此时被动断开连接方会进入LAST_ACK的状态,等待最后一个ACK的到来,以确定客户端收到了服务器发送的FIN
  • 第四次挥手主动断开连接方收到被动断开连接方发来的FIN结束报文段之后,进入TIME_WAIT状态,同时发送给被动断开连接方最后一个ACK响应,告诉被动断开连接方我收到了你的结束报文段,并等待2倍的报文最大生存时间之后,确保被动断开连接方收到最后一个ACK,再进入CLOSED状态,完成连接关闭。同时被动断开连接方收到最后一个ACK应答之后,状态转换为CLOSED状态,彻底关闭连接。

流程如图所示
在这里插入图片描述
这个断开连接的过程, 通常称为四次挥手

2. 简单的TCP网络程序

在这里插入图片描述

2.1 TCP的socket API详解

2.1.1 监听

当调用监听之后,意味着告诉操作系统,当前进程可以接收新的TCP连接了。
换句话说,告诉操作系统,当前进程可以开始接收客户端的三次握手请求了。

// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
参数:
    sockfd: 套接字描述符
    backlog: 已完成连接队列的大小,引申含义:并发连接数
  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5);
  • listen()成功返回0, 失败返回-1。

2.1.2 获取连接

如果服务端在获取连接的时候,目前没有新的连接,则该接口会阻塞等待。

// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
    sockfd: 套接字描述符,传递的是侦听套接字
    addr: 对端地址信息
    addrlen: 对端地址信息长度
返回值: 返回为新连接创建的套接字描述符
  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求, 就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数, accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL, 表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度。

2.1.3 建立连接

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
    sockfd :套接字描述符
    addr :服务端的地址信息
    addrlen : 地址信息长度
  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0, 出错返回-1。

2.1.4 发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
    sockfd: 套接字描述符
        客户端: socket函数的返回值
        服务端: 服务端为客户端新创建的套接字描述符
    buf: 待发送的数据
    len: 发送数据的长度
    flags: 0 表示阻塞发送

2.1.5 接收数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags) ;
参数:
    sockfd: 套接字描述符
        客户端: socket函数的返回值
        服务端: 服务端为客户端新创建的套接字描述符
    buf: 缓冲区,用来接收数据
    len: 接收的最大能力
    flags : 0 表示阻塞接收,MSG_PEEK 表示探测接收
返回值:
    大于0: 表示接收了多少字节的数据
    等于0: 表示对端断开了连接,意味着对端调用了close
    小于0: 接收失败

在这里插入图片描述

2.2 客户端程序

创建套接字,发起连接,发送数据,接收数据,关闭套接字。

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

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(19999);
    dest_addr.sin_addr.s_addr = inet_addr("42.192.103.123");

    int ret = connect(sockfd, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
    if(ret < 0)
    {
        perror("connect");
        return -1;
    }

    while(1)
    {
        char buf[1024] = {0};
        strcpy(buf, "i am client");
        ret = send(sockfd, buf, strlen(buf), 0);
        if(ret < 0)
        {
            perror("send");
            return -1;
        }

        memset(buf, '\0', sizeof(buf));

        ret = recv(sockfd, buf, sizeof(buf) - 1, 0);
        if(ret < 0)
        {
            perror("recv");
            return -1;
        }
        else if(ret == 0)
        {
            printf("peer shutdown\n");
            close(sockfd);
            return 0;
        }

        printf("svr say: %s\n", buf);
    }

    close(sockfd);
    return 0;
}

2.3 服务端程序

创建套接字,绑定地址信息,监听,获取连接,接收数据,发送数据,关闭套接字。

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

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;                       
    }
	
	int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口重用

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(19999);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");

    int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return -1;                        
    }

    ret = listen(sockfd, 5);
    if(ret < 0)
    {
        perror("listen");
        return -1;                        
    }

    // 没有新连接时,该接口会阻塞
    int newsockfd = accept(sockfd, NULL, NULL);
    if(newsockfd < 0)
    {
        perror("accept");
        return -1;
    }

    while(1)
    {
        char buf[1024] = {0};

        ret = recv(newsockfd, buf, sizeof(buf) - 1, 0);
        if(ret < 0)
        {
            perror("recv");
            return -1;
        }
        else if(ret == 0)
        {
            printf("peer shutdown\n");// 对端关闭
            close(newsockfd);
            return 0;
        }

        printf("cli say: %s\n", buf);

        memset(buf, '\0', sizeof(buf));
        const char* str = "i am server"; // str是临时变量
        strncpy(buf, str, strlen(str)); // 不会越界

        sleep(2);

        ret = send(newsockfd, buf, strlen(buf), 0);
        if(ret < 0)
        {
            perror("send");
            return -1;
        }
    }

    close(newsockfd); 
    close(sockfd);
    return 0;
}

2.4 测试多个连接的情况

再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信。
分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求。
我们当前的这个TCP, 只能处理一个连接, 这是不科学的。

3. TCP多进程版本

通过每个请求, 创建子进程的方式来支持多连接。
在这里插入图片描述

// tcp_process_server.cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(19999);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");

    int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return -1;
    }

    ret = listen(sockfd, 5);
    if(ret < 0)
    {
        perror("listen");
        return -1;
    }

    while(1)
    {
        int newsockfd = accept(sockfd, NULL, NULL);
        if(newsockfd < 0)
        {
            perror("accept");
            return -1;
        }

        pid_t pid = fork(); // 创建子进程
        if(pid < 0)
        {
            perror("pid");
            close(newsockfd);
            close(sockfd);
            return -1;
        }
        else if(pid == 0)
        {
            //child 
            close(sockfd);
            while(1)
            {
                char buf[1024] = {0};

                ret = recv(newsockfd, buf, sizeof(buf) - 1, 0);
                if(ret < 0)
                {
                    perror("recv");
                    return -1;
                }
                else if(ret == 0)
                {
                    printf("peer shutdown\n");
                    close(newsockfd);
                    return 0;
                }

                printf("cli say: %s\n", buf);

                memset(buf, '\0', sizeof(buf));
                const char* str = "i am server";
                strncpy(buf, str, strlen(str));

                sleep(2);

                ret = send(newsockfd, buf, strlen(buf), 0);
                if(ret < 0)
                {
                    perror("send");
                    return -1;
                }
            }
            close(newsockfd);
        }
        else
        {
            //father
            close(newsockfd);
        }
    }

    close(sockfd);
    return 0;
}

4. TCP多线程版本

通过每个请求, 创建一个线程的方式来支持多连接。

//tcp_thread_server.cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

// 从堆上开辟newsockfd_结构体
struct NewConnectSockFd
{
    int newsockfd_;
};

// 工作线程入口函数
void* TestTcpStart(void* arg)
{
    pthread_detach(pthread_self()); // 分离自己

    struct NewConnectSockFd* nf = (struct NewConnectSockFd*)arg;

    while(1)
    {
        char buf[1024] = {0};

        int ret = recv(nf->newsockfd_, buf, sizeof(buf) - 1, 0);
        if(ret < 0)
        {
            perror("recv");
            close(nf->newsockfd_);
            delete nf;
            return NULL;
        }
        else if(ret == 0)
        {
            printf("peer shutdown\n");
            close(nf->newsockfd_);
            delete nf;
            return NULL;
        }

        printf("cli say: %s\n", buf);

        memset(buf, '\0', sizeof(buf));
        const char* str = "i am server";
        strncpy(buf, str, strlen(str));

        sleep(2);

        ret = send(nf->newsockfd_, buf, strlen(buf), 0);
        if(ret < 0)
        {
            perror("send");
            close(nf->newsockfd_);
            delete nf;
            return NULL;
        }
    }

    delete nf; // 在所有退出的地方释放
    return NULL;
}

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(19999);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");

    int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind");
        return -1;
    }

    ret = listen(sockfd, 5);
    if(ret < 0)
    {
        perror("listen");
        return -1;
    }

    while(1)
    {
        // 主线程
        int newsockfd = accept(sockfd, NULL, NULL);
        if(newsockfd < 0)
        {
            perror("accept");
            return -1;
        }

        struct NewConnectSockFd* ncsf = new NewConnectSockFd();
        ncsf->newsockfd_ = newsockfd;// 刚接收回来的赋值给ncsf

        pthread_t tid; //线程标识符
        // 创建工作线程
        ret = pthread_create(&tid, NULL, TestTcpStart, (void*)ncsf); // 将结构体传入
        if(ret < 0)
        {
            delete ncsf;
            perror("pthread_create");
            return -1;
        }
    }

    close(sockfd);
    return 0;
}

标签:return,addr,编程,TCP,ret,sockfd,接字,buf,连接
来源: https://blog.csdn.net/weixin_44472033/article/details/111769576

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有