socket 编程流程
#include
#include
int listen(int sockfd, int backlog);
在网络通信中, 客户端通常处于主动的一方, 而服务器则是被动的一方, 服务器是被连接的, 所以他要时刻准备着被连接, 所以就需要调用 listen() 来监听, 等着被连接.
listen() 函数的主要作用就是将 socket() 函数得到的 sockfd 变成一个被动监听的套接字, 用来被动等待客户端的连接, 而参数 backlog 的作用就是设置连接队列的长度
三次握手,建立连接不是 listen() 函数完成的, 而是内核完成的, listen() 函数只是将 sockfd 和 backlog 告诉内核, 然后就返回了
之后, 如果有客户端通过 connect() 发起连接请求, 内核就会通过三次握手建立连接, 然后将建立好的连接放到一个队列中, 这个队列称为: 已完成连接队列
#include
#include
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
通常客户端通过 connect() 函数来向服务端主动发起连接, 但是建立连接也不是这个函数完成的, 而是由内核完成的, 这个函数仅仅是通知内核通过三次握手建立连接, 然后将结果返回给这个函数.
这个函数默认会一直阻塞, 直到内核连接建立成功或者超时失败才返回(但一般这个过程很快)
所以说, 服务器端通过 listen() 函数来通知内核建立连接, 客户端通过 connect() 函数来通知内核建立连接
因此, 在 listen() 之后, 连接就已经建立好了, 建立好的连接存储在已完成连接队列中
这里还需要再分析一下 listen() 函数的第二个参数 backlog, 实际上, 内核为每一个监听套接字维护两个队列
未完成连接队列
其中存储着尚未建立连接的套接字
已完成连接队列
存储着已经完成连接的套接字
一般认为 backlog 参数的大小是这两个队列大小之和, 收到客户端的连接请求之后, 内核创建一个套接字存储在未完成连接队列中, 来进行三次握手建立连接
连接建立完成以后, 这个套接字就加到已完成连接队列的队尾, 服务器从已完成连接队列中取走一个, 又空出一个位置, 然后已经完成连接的套接字由补充进来, 就这样实现动态平衡
所以说, 如果在 listen 之后不进行 accept , connect 也是会成功返回的, 其实此时连接就已经建立好了
可以验证一下
服务器端
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
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 状态, 连接已经建立好了
#include
#include
int accept(int sockfd,
struct sockaddr *addr,
socklen_t *addrlen);
所以, accept() 函数的作用就是在已完成连接队列中取出一个已经建立好的连接
如果这个队列中已经没有已完成连接的套接字, 那么 accept() 就会一直阻塞, 直到取得一个已经建立连接的套接字
如果服务器来不及调用 accept() 取走队列中已经建立好的连接, 导致队列中的连接满了, 会怎么样呢 ?
这取决于内核的具体实现, 在Linux中会延时建立连接.