目录
三、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函数从等待连接的队列中取出一个连接,向连接打开出口门,此时通道正式建立。