关于linux上socket通信的一些理解

目录

一、简单通信代码

二、客户端与服务端socket文件描述符

三、listen函数和accpet返回的socket是否是同一个

四、多个连接

五、总结:

今天在看CSAPP第十一章的时候,对于整个socket通信非常迷惑,比如listen到底是在干嘛,为什么不能直接用accept函数?accpet返回的新的socket文件描述符与前面的socket文件描述符指向的是同一个socket文件吗?

为此,我分别在网上找到了简易的服务端和客户端之间socket通信代码进行修改,进行模拟通信。

首先需要明白的是,linux上万物皆文件,每一个socket都被linux内核看作一个可操作的文件,并分配一个文件描述符。

一、简单通信代码

服务端 server.c

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

int main() {

    //创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    printf("sockfd = %d\n", sockfd);//此处打印获得的服务端socket文件描述符

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址

    serv_addr.sin_port = htons(1234);  //端口
    bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	
    while (1) {
        //进入监听状态,等待用户发起请求
        listen(sockfd, 20);


        //接收客户端请求
        struct sockaddr_in clnt_addr;
        socklen_t clnt_addr_size = sizeof(clnt_addr);
        int clnt_sock = accept(sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
       
	    printf("accept_sockfd:%d\n",clnt_sock);//打印accpet返回的新的文件描述符
        //arnold add
    //自己打印信息
        printf("received request from client!\n");

        //向客户端发送数据
        char str[] = "Hello World!";
        write(clnt_sock, str, sizeof(str));


        //close(clnt_sock); //这里为了后面的测试,不关闭socket文件描述符

    }

    //关闭套接字
    //close(clnt_sock);
    close(sockfd);



    return 0;
}

客户端 test.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
#define MAXLINE 40
void sigint_handler(int sig){ //自己写的信号处理函数
	printf("Caught\n");
}
int main() {
    //创建套接字
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址

    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址

    serv_addr.sin_port = htons(1234);  //端口
    
    connect(clientfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//与服务器建立连接

    printf("clientfd = %d\tsend successfully\n",clientfd);

    //读取服务器传回的数据
    char buffer[MAXLINE];
    read(clientfd, buffer, MAXLINE);

    printf("Get message form server: %s\n", buffer);
    
    if(signal(SIGINT,sigint_handler) == SIG_ERR){
    	fprintf(stderr,"%s: %s\n","signal error.",strerror(errno));
    	exit(0);
    }
    pause();//为了后面的测试挂起,不让socket文件关闭
    //关闭套接字
    close(clientfd);



    return 0;
}

 注意下面的测试我是在同一个虚拟机上完成的,即客户端代码和服务端代码在同一虚拟机上运行

客户端效果

 

服务端效果

 这个时候我们可以看到,客户端socket文件描述符为3,服务端开始绑定的socket文件描述符也是3(即listen函数使用的socket文件描述符),accept函数返回的socket文件描述符是4,那么服务端的两个socket文件描述符指向的socket文件是一个文件吗?客户端的socket文件描述符和服务端的又有什么关系呢,它们是否是同一呢?

二、客户端与服务端socket文件描述符

首先,客户端的socket文件描述符和服务端的socket文件描述符没有任何关系。因为每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。而server和test属于两个不同的进程,因此它们拥有不同的描述符表,虽然文件描述符数值相等都为3,但是它们索引的是不同表。它们恰好数值都为3,也只是因为一个巧合。linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2),它们都属于进程除去这三个文件之外的第一个文件,所以描述符为3.

 

三、listen函数和accpet返回的socket是否是同一个

接下来是关于两个服务端的socket文件描述符是否指向同一个socket文件问题。我们使用sockstat命令进行查看

 发现有两个与server相关联的socket,并且server的pid为7412

再使用ls -l /proc/7412/fd命令查看7412进程下的所有文件描述符,其中的3和4就是我们要找的socket文件描述符,我们知道了它们分别指向socket:202925和socket:202926

 我们再次使用more /proc/net/tcp命令,通过inode(也就是上面的202925和202926)找到这两个socket的端点

 此处我们将上图结果放大截取只我们需要的部分

 local_address:代表本地的端点值(ip:端口号)

rem_address:代表请求的远端的端点值

上面使用的都是网路字节序,下面翻译成点分十进制

local_address都是127.0.0.1,端口号都是1234(我们在前面的程序里设置的),但是rem_address并不相同,socket文件描述符为3对应的是0.0.0.0:0,描述符为4对应的是127.0.0.1:34964,根据tcp的四元组确定唯一,所以listen函数的文件描述符和accept函数返回的描述符指向的并不是同一个socket文件。

四、多个连接

那么,同一个客户端和同一个服务端能有多个TCP连接吗?

我们在test1中建立两次连接,使用同一个socket描述符,其他内容与test一致

    connect(clientfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//与服务器建立连接

    printf("clientfd1 = %d\tsend successfully\n",clientfd);
    connect(clientfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//与服务器建立连接

    printf("clientfd2 = %d\tsend successfully\n",clientfd);

运行test1,我们发现收到的来自服务端的hello world变成了乱码

查看服务端,发现test1中的两个连接请求都没有被服务端接收,连接根本没有建立

 

 那如果我创建两个clientfd呢?

    //创建套接字
    int clientfd1 = socket(AF_INET, SOCK_STREAM, 0);
    int clientfd2 = socket(AF_INET, SOCK_STREAM, 0);
    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址


    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址


    serv_addr.sin_port = htons(1234);  //端口
  
    
    connect(clientfd1, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//与服务器建立连接
    connect(clientfd2, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//与服务器建立连接
    
    printf("clientfd1 = %d\tsend successfully\n",clientfd1);
    printf("clientfd2 = %d\tsend successfully\n",clientfd2);

    //读取服务器传回的数据
    char buffer1[MAXLINE];
    char buffer2[MAXLINE];
    read(clientfd1, buffer1, MAXLINE);
    read(clientfd2, buffer2, MAXLINE);

    printf("Get message1 form server: %s\n", buffer1);
    printf("Get message2 form server: %s\n", buffer2);

 

两次连接成功了,用sockstat查看,发现两个连接虽然是运行在同一个进程中,但是使用了不同的端口号。说明linux会为目的端点相同的每一次连接分配一个临时的端口号,以此来区别tcp的四元组,不同的socket值。

五、总结:

每一次的连接就像建立一个通道,客户端和服务端的socket就像通道两边的门,由一个叫做socket文件描述符的东西进行区别。socket的值就像我们门的门牌号,即ip+端口值,能让我们找到这扇门。客户端每次发出一个connect请求,相当于想建立一个通道,并且给出了通道的目的地,打开了进入通道的入口门,但是目的地的出口门并没有被打开。请求被服务端目的地的listen函数发现,listen将该连接放入到一个等待连接的队列中,但是并不处理该连接,所以此时通道还是没有被打通。accpet函数从等待连接的队列中取出一个连接,向连接打开出口门,此时通道正式建立。

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值