listen(), connect(), accept() 三者的关系
listen(), connect(), accept() 三者的关系
socket 编程流程
listen()函数
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
在网络通信中, 客户端通常处于主动的一方, 而服务器则是被动的一方, 服务器是被连接的, 所以他要时刻准备着被连接, 所以就需要调用 listen() 来监听, 等着被连接.
listen() 函数的主要作用就是将 socket() 函数得到的 sockfd 变成一个被动监听的套接字, 用来被动等待客户端的连接, 而参数 backlog 的作用就是设置连接队列的长度
三次握手,建立连接不是 listen() 函数完成的, 而是内核完成的, listen() 函数只是将 sockfd 和 backlog 告诉内核, 然后就返回了
之后, 如果有客户端通过 connect() 发起连接请求, 内核就会通过三次握手建立连接, 然后将建立好的连接放到一个队列中, 这个队列称为: 已完成连接队列
connect()函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
通常客户端通过 connect() 函数来向服务端主动发起连接, 但是建立连接也不是这个函数完成的, 而是由内核完成的, 这个函数仅仅是通知内核通过三次握手建立连接, 然后将结果返回给这个函数.
这个函数默认会一直阻塞, 直到内核连接建立成功或者超时失败才返回(但一般这个过程很快)
所以说, 服务器端通过 listen() 函数来通知内核建立连接, 客户端通过 connect() 函数来通知内核建立连接
因此, 在 listen() 之后, 连接就已经建立好了, 建立好的连接存储在已完成连接队列中
这里还需要再分析一下 listen() 函数的第二个参数 backlog, 实际上, 内核为每一个监听套接字维护两个队列
-
未完成连接队列
其中存储着尚未建立连接的套接字
-
已完成连接队列
存储着已经完成连接的套接字
一般认为 backlog 参数的大小是这两个队列大小之和, 收到客户端的连接请求之后, 内核创建一个套接字存储在未完成连接队列中, 来进行三次握手建立连接
连接建立完成以后, 这个套接字就加到已完成连接队列的队尾, 服务器从已完成连接队列中取走一个, 又空出一个位置, 然后已经完成连接的套接字由补充进来, 就这样实现动态平衡
所以说, 如果在 listen 之后不进行 accept , connect 也是会成功返回的, 其实此时连接就已经建立好了
可以验证一下
服务器端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
const int port = 9999;
int main( )
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = INADDR_ANY;
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
perror("sock");
return -1;
}
int ret = bind(sock, (struct sockaddr *)&server,
sizeof(struct sockaddr_in));
if(ret == -1)
{
perror("bind");
close(sock);
return -1;
}
ret = listen(sock, 10);
if(ret == -1)
{
perror("listen");
close(sock);
return -1;
}
sleep(10);
system("netstat -anp | grep 9999");
while(1);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
const int port = 9999;
const char* server_ip = "192.168.2.128";
int main()
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(server_ip);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
perror("socket");
return -1;
}
int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("connect");
close(sock);
return -1;
}
system("netstat -anp | grep 9999");
while(1);
return 0;
}
先运行服务器, 再运行客户端, 结果如下
此时已经处于 Establish 状态, 连接已经建立好了
accept()函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd,
struct sockaddr *addr,
socklen_t *addrlen);
所以, accept() 函数的作用就是在已完成连接队列中取出一个已经建立好的连接
如果这个队列中已经没有已完成连接的套接字, 那么 accept() 就会一直阻塞, 直到取得一个已经建立连接的套接字
如果服务器来不及调用 accept() 取走队列中已经建立好的连接, 导致队列中的连接满了, 会怎么样呢 ?
这取决于内核的具体实现, 在Linux中会延时建立连接.
推荐阅读
-
Oracle中的Connect/session和process的区别及关系介绍
-
Oracle中的Connect/session和process的区别及关系介绍
-
浅谈jpa、hibernate与spring data jpa三者之间的关系
-
关于HTML、CSS、JavaScript三者关系的简述
-
人工智能、机器学习和深度学习三者的关系与应用
-
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)
-
同步异步多线程这三者关系,你能给面试官一个满意的回答吗?
-
构造函数与实例对象和原型对象三者间的关系
-
c运行库、c标准库、windows API三者之间的关系和区别
-
解析GOROOT、GOPATH、Go-Modules-三者的关系