Linux:TCP Socket编程(代码实战)

Posted It‘s so simple

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux:TCP Socket编程(代码实战)相关的知识,希望对你有一定的参考价值。

目录


1. TCP的Socket编程

1.1 TCP的编程流程

和UDP编程一样,我们在考虑TCP的时候也要考虑到接收方和发送方,也就是是说要考虑接收方和发送方的,但是由于TCP是有连接,可靠有序且面向字节流的传输层协议,因此,TCP的编程流程要比UDP复杂一些。

用图来表示的话如下:

1.2 TCP Socket的接口

创建套接字socket()、关闭套接字close()和绑定地址信息bind()这三个函数均和UDP的一样,可以看我之前写的这篇文章

1.2.1 服务端监听连接接口

int listen(int sockfd, int backlog);

参数:

  • sockfd:套接字描述符
  • backlog:已完成连接队列的大小

    注:服务端和客户端在进行三次握手的过程中,会存在两个队列,一个是已完成连接的队列,一个是未完成连接的队列,未完成连接的队列中存储的是没有完成三次握手的连接;而已完成连接的队列中存储的是已经完成三次连接,并且等待被服务端"accept"的连接,或者也可以理解为当前连接状态为"ESTABLISHED"的连接。

backlog参数的具体理解:

由于TCP连接首先要进行三次握手,而想要和服务端建立连接的客户端却有很多个,在同一时间内可能会有多个客户端再和服务端进行三次握手。而backlog控制的就是已完成连接队列的大小,因此backlog影响了服务端在一瞬间的连接的接收能力,或者说影响了服务端并发接收新连接的能力,注意不能将其理解为 服务端建立连接的数量。

1.2.2 服务端获取新连接接口

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

作用:

从已完成连接队列中获取已经完成三次握手的连接。(当没有连接的时候,调用该接口会发送阻塞)

参数:

  • sockfd:套接字描述符
  • addr:客户端的地址信息结构
  • addrlen:客户端的地址信息结构的长度

返回值:(非常重要)

成功则返回新连接的套接字描述符,失败则返回-1。

accept返回值的深入理解:

如图:

这就是为什么返回值如此重要原因,因为他关系着后面收发数据时所对应在服务端中的缓冲区时哪一个。

1.2.3 客户端发起连接接口

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:套接字描述符
  • addr:服务端的地址信息结构
  • addrlen:服务端的地址信息结构的长度

返回值:

成功则返回0,失败则返回小于0的数。

1.2.4 发送数据接口

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • sockfd:套接字描述符
  • buf:待要发送的数据
  • len:待要发送数据的长度
  • flags:0为阻塞发送,MSG_OOB为发送带外数据

带外数据:即在紧急情况下所产生的数据,会越过前面进行排队的数据优先进行发送。

返回值:

成功则返回的字节数量,失败则返回-1。

1.2.5 接收数据接口

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • sockfd:套接字描述符
  • buf:将接收到的数据放到buf中
  • len:接收数据的长度
  • flags:0为阻塞接收,即如果没有发送数据,则recv接口会阻塞

返回值:

  • >0:正常接收了多少字节的数据
  • ==0:对端将连接关闭(即对端机器调用了close函数关闭了套接字)
  • >0:接收失败

1.3 三次握手的简单验证

由于三次握手是操作系统内核自动完成的,因此我们只需要给出服务端的的代码到listen接口即可。

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

using namespace std;


int main()

    //1. 创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sockfd < 0)
    
        cout << "TCP socket failed" << endl;
        return 0;
    

    //2.绑定地址信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    
        cout << "bind failed" << endl;
        return 0;
    

    //3.进行侦听(告诉OS内核可以与客户端建立连接了)
    //listen函数中blocklog参数,设定的是已完成三次握手并已建立连接的队列的容量
    ret = listen(sockfd,1);
    if(ret < 0)
    
        cout << "listen failed" << endl;
        return 0;
    
    while(1)
    ;
    return 0;


要测试三次握手是否成功,我们不需要编写客户端代码进行验证,而是借助windows下的一个工具telnettelnet可以模仿TCP进行三次握手,就只是单纯的建立连接,其他什么都不会做。

使用telnet + 公网ip + 端口号即可对其进行测试,需要注意的是,这条命令是在windows下的cmd窗口运行的。

首先我们需要将服务端运行起来然后再使用telnet进行测试

当一回车之后,会新弹出一个telnet的窗口,代表三次握手成功。

下面给出两种失败的情况
情况1:端口不存在

情况2:公网ip错误

我们多使用telnet建立几次连接,然后看看当前服务器端口的使用情况。
这里总共使用telnet与当前服务器进行了三次的连接,即进行了三个三次握手,而我们在代码中设置的已完成连接的队列的大小为1,我们可以使用netstat -anp | grep 18989查看18989端口的使用情况。

我们可以清楚的看到该服务端建立了两次连接,但是我们设置的backlog为1,按理说只能连接一次,那这是什么情况呢?原因是操作系统内核的问题,内核中的代码逻辑是这样的

// q为 已完成连接队列   ,capacity_为当前我们设置的backlog的大小
if(q.size() < capacity_)

	//不建立连接 

因此,根据这个逻辑来分析,他会建立两次连接,是正常的,并不是我们代码的问题。
但是有一点需要注意的是,在验证三次握手的时候,我们服务器端的代码绝对不能用accept来接收,因为一旦用accept接收之后,就会从当前已完成队列中取走一个连接,导致我们看到的连接结果会多一条,如图:

2. 代码实战

本次TCP的代码有三种形式:单进程版本、多进程版本和多线程版本,但是这些版本均只是针对服务端而言,其中单进程和多进程的客户端的代码都是相同的,因此在这里我们首先给出客户端的代码。

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

using namespace std;

/*
 * 1. 创建套接字
 * 2. 建立连接请求
 * 3. TCP三次握手建立连接----OS会自动进行,不需要我们进行控制
 * 4. 接收数据、发送数据
 * 5. 关闭套接字
 */ 

int main()

    //1. 创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sockfd < 0)
    
        cout << "TCP socket failed" << endl;
        return 0;
    

    //2.进行连接
    //0.0.0.0:任意网卡地址,注意不是任一,它的含义就是哪个网卡地址都行
    //当服务器端绑定的地址为0.0.0.0,那么我们客户端想要与服务端建立连接的时候
    //有两种方法: 
    //1.若是运行服务器的这台机器本身是能够上网的,
    // 那么我们只需要连接它的网卡地址中的任意一个就行,(0.0.0.0为任意地址)
    //2.若是运行服务器的这台机器是要经过类似于腾讯云、阿里云这样的
    // 云服务器读对私网ip进行转换才能上网的,那么我们就需要连接它的公网ip。
    
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    //此处我们使用的公网ip
    addr.sin_addr.s_addr = inet_addr("118.89.67.215");

    int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    
        cout << "connnect failed" << endl;
        return 0;
    

    //3.接收和发送数据
    while(1)
    
        //这里接收数据和发送数据是没有顺序而言的
        //因为已经经过三次握手,客户端和服务端的连接以及建立成功了
        char buf[1024] = 0;
        
        //发送数据
        sprintf(buf,"hello server,i am client2\\n");
        ssize_t send_ret = send(sockfd,buf,strlen(buf),0); 
        if(send_ret < 0)
        
            cout << "send failed" << endl;
            continue;
        
        memset(buf,'\\0',sizeof(buf));

        ssize_t recv_size = recv(sockfd,buf,sizeof(buf)-1,0);
        if(recv_size < 0)
        
            cout << "recv failed" << endl;
            continue;
        
        else if(recv_size == 0)
        
            cout << "Peer close" << endl;
            close(sockfd);
            return 0;
        
        
        cout << "recv data is : " << buf << endl;


    

    close(sockfd);
    return 0;


2.1 单进程版本的TCP代码

单进程版本的服务端代码如下:

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

using namespace std;

/*
 * 1. 创建套接字
 * 2. 绑定地址信息
 * 3. 进行监听--(告诉OS内核可以接收客户端发送的新请求了并且监听新连接的到来)
 * 4. TCP三次握手建立连接----OS会自动进行,不需要我们进行控制
 * 5. 接收新连接
 * 6. 接收数据、发送数据
 * 7. 关闭套接字
 */ 

int main()

    //1. 创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sockfd < 0)
    
        cout << "TCP socket failed" << endl;
        return 0;
    

    //2.绑定地址信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    
        cout << "bind failed" << endl;
        return 0;
    

    //3.进行侦听(告诉OS内核可以与客户端建立连接了)
    //listen函数中blocklog参数,设定的是已完成三次握手并已建立连接的队列的容量
    ret = listen(sockfd,1);
    if(ret < 0)
    
        cout << "listen failed" << endl;
        return 0;
    

    //4.接收新连接
    struct sockaddr_in client_addr;
    socklen_t socklen = sizeof(client_addr);
    int new_sockfd = accept(sockfd,
    			(struct sockaddr *)&client_addr,&socklen);

    if(new_sockfd < 0)
    
        cout << "accept failed" << endl;
        return 0;
    

    printf("accept success: %s:%d, and new_sockfd is %d\\n",
    inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),
    			new_sockfd);

    //5.接收和发送数据
    while(1)
    
        //这里接收数据和发送数据是没有顺序而言的
        //因为已经经过三次握手,客户端和服务端的连接以及建立成功了
        
        char buf[1024] = 0;

        ssize_t recv_size = recv(new_sockfd,buf,sizeof(buf)-1,0);
        if(recv_size < 0)
        
            cout << "recv failed" << endl;
            continue;
        
        else if(recv_size == 0)
        
            cout << "Peer close" << endl;
            close(new_sockfd);
            close(sockfd);
            return 0;
        
        
        cout << "accrpt data is : " << buf << endl;

        //发送数据
        memset(buf,'\\0',sizeof(buf));
        sprintf(buf,"hello lient,i am server,i am %s:%d\\n",
        		inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
        		
        ssize_t send_ret = send(new_sockfd,buf,strlen(buf),0); 
        if(send_ret < 0)
        
            cout << "send failed" << endl;
            continue;
        

    

    close(sockfd);
    return 0;

分析完代码后,我们来看看客户端与服务端之间的通信吧!!

分别运行客户端和服务端所得到的结果如下:
服务端的结果:

客户端的结果:

看上去好像成功了,但是这只是一个客户端与服务器通信的情况,如果是多个客户端呢?他的情况是怎样的?在上面的基础上,我们再运行一个客户端,看看其结果如何:

我们发现第二个客户端完全不运行,这是什么情况呢?我们使用ps aux | grep []pstack []命令来对其进行分析。

稍微对服务端代码进行分析就可知,因为服务端accept在while循环外面,所以只能从已完成连接队列中拿出一次连接进行通信,而第二个客户端进来后,虽然和服务端三次握手成功建立了连接,但是并没有被服务端接收其连接,因此当客户端代码走到recv逻辑的时候就会发生阻塞,因为客户端是不会给他回复任何消息的。

那能否将服务端accept代码放到while循环里面?答案肯定也是不行的,稍微分析一下,当while循环每次走到accept逻辑的时候,都要从已完成连接队列中获取连接,当队列为空的时候就会阻塞掉,因此,当有两个客户端的时候,它们之间只能通信一次,然后就会陷入到accept阻塞的逻辑中。

稍微总结一下,单进程版本的TCP代码当有多个客户端与其服务器进行通信的时候,依据代码逻辑的不同,可能会产生两种阻塞:recv阻塞和accept阻塞。

那么该如何解决呢?有三种解决办法。

  • 多进程版本的TCP代码
  • 多线程版本的TCP代码
  • 使用多路转接的技术(这个在后面会进行讲解)

2.2 多进程版本的TCP代码

再写多进程代码的时候,一定要注意的点有:父进程只管accept,子进程只管send和recv数据,因为父子进程的进程虚拟空间是几乎相同的,父进程从已完成连接队列中得到的套接字,子进程也能拥有。

但是一定要注意到进程等待,如果子进程先于父进程退出,且父进程没有进行相应的进程等待,那么该子进程就会变成僵尸进程。但是如果我们直接在父进程的逻辑中加上wait函数是行不通的,因为,随着客户端的增多,父进程创建出的子进程也逐渐变多,wait函数会捕捉子进程退出时所发出的SIGCHLD信号,但是是不知道捕捉的是哪一个进程的。

因此,在这里,我们使用signal函数将SIGCHLD该信号处理重新用我们自己的逻辑定义一下即可。

服务端的代码如下:

#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <netinet/in.h>
#include <string.h>
#include <iostream>

using namespace std;

void signalcallBack(int)

    wait(NULL);
    return;


int main()

    signal(SIGCHLD,signalcallBack);
   
    //创建套接字
    int listen_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(listen_sock < 0)
    
        cout << "socket failed" << endl;
        return 0;
    
    
    //绑定地址信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int ret = bind(listen_sock,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    
        cout << "bind failed" << endl;
        return 0;
    

    //帧听
    ret = listen(listen_sock,1);
    if(ret < 0)
    
        cout << "listen failed" << endl;
        return 0;
    

    while(1)
    
        struct sockaddr_in peer_addr;
        socklen_t peerlen;
        int new_sockfd = accept(listen_sock,
        				(struct sockaddr *)&peer_addr,&peerlen);
        if(new_sockfd < 0)
        
            cout << "accept failed" << endl;
            return 0;
        
        pid_t fork_ret = fork();
        if(fork_ret < 0)
        
            cout << "fork failed" << endl;
            continue;
        
        else if(fork_ret == 0)
        
            close(listen_sock);
            //child
            //在子进程中只实现接收和发送数据
            while(1)
            
                char buf[1024] = 0;
             ssize_t recv_size = recv(new_sockfd,buf,sizeof(buf)-1,0);
                if(recv_size < 0)
                
                    cout << "recv failed" << endl;
                    continue;
                
                else if(recv_size == 0)
                
                    cout << "peer failed" << endl;
                    break;
                

                cout << "i recv: " << buf << endl;

                //接收数据
                memset(buf,'\\0',sizeof(buf));
                sprintf(buf,"hello client,%s:%d\\n",
                		inet_ntoa(peer_addr.sin_addr),
                		ntohs(peer_addr.sin_port));
                ssize_t send_size = send(new_sockfd,buf,strlen(buf),0);
                if(send_size < 0)
                
                    cout << "send failed" << endl;
                    break;
                
            
        

        else
        
            //father
            //要进行进程等待,防止子进程变成僵尸进程
            //但是还存在着的问题是,该父进程可能会创建多个子进程
            //若是直接在父进程中进行进程等待的话,就意味着若只有一个子进程退出
            //父进程也会随之等待到资源,然后continue上去会陷入到accept的逻辑中
            //因此,我们需要设置信号,自定义信号的处理逻辑,每当有一个sigchild
            //信号发送出来的时候,我们让其在自定义的处理函数进行进程等待即可
            continue;
        
    

    return 0;

可能是我上传的图片太多了,上传图片总是失败,所以这里就不再进行结果的验证了。

2.3 多线程版本的TCP代码

多线程和多进程一样,也需要考虑几点,要将新的套接字传入线程入口函数,为此我们要在堆上申请一个空间用来存储新的套接字;为了不让线程进行等待,我们可以在线程入口函数中直接将自己线程分离掉即可;最后一点,一定要注意退出时对内存进行释放,关闭套接字,避免内存泄漏。

我在这里用一个类将TCP的各个接口给封装了起来,换句话说,到时候可以在客户端或服务端直接调用封装的类的接口即可实现逻辑。

封装的tcp类:

//tcp.hpp
#Linux:UDP Socket编程(代码实战)

Linux:UDP Socket编程(代码实战)

socket编程实战

iOS Socket/Tcp编程 GCDAsyncSocket的实战(带回调)

Linux-TCP编程流程-Socket编程-单线程实现TCP客户端和服务端交互-多进程实现TCP客户端和服务端交互

Linux----网络编程socket