1. TCP服务器端的默认函数调用顺序
TCP服务器端多按照以下顺序进行函数调用:
1.1 进入等待请求状态
当调用完bind函数给套接字分配了地址,接下来就要通过调用listen函数进入等待连接请求状态。只有调用listen函数才能使客户端进入到可发出连接请求的状态,从而使客户端能够调用connect函数。
#include <sys/socket.h>
int listen(int sock , int backlog);
//成功时返回0,失败时返回-1
//sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)。
//backlog:连接请求等待队列(Queue)的长度,表示有n个连接请求进入队列。
“服务器端处于等待连接请求状态”一般是指在客户端请求连接(服务器端)时,受理连接前一直使请求处于等待状态。
1.2 受理客户端连接请求
调用listen函数后,若有新的连接请求,则应按序受理。下面的函数能够帮助自动创建套接字,并连接到发起请求的客户端。
#include <sys/socket.h>
int accept(int sock , struct sockaddr * addr , socklen_t * addrlen);
//成功时返回创建的套接字文件描述符,失败时返回-1
//sock:服务器套接字的文件描述符
//addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
//addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
accept函数用来受理等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。需强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。
代码回顾(Hello World服务器端):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello World!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 第21行:服务器端实现过程中先要创建套接字。第21行创建套接字,但此时的套接字尚非真正的服务器端套接字。
- 第25-31行:为了完成套接字地址分配,初始化结构体变量并调用 bind 函数。
- 第33行:调用 listen 函数进入等待连接请求状态。连接请求等待队列的长度设置为5。此时的套接字才是服务器端套接字。
- 第37行:accept 函数从队头取 1 个连接请求与客户端建立连接,并返回创建的文件描述符。另外,调用 accept 函数时若等待队列为空,则accept 函数不会返回,直到队列中出现新的客户端连接。
- 第41、42行:调用 write 函数向客户端传输数据,调用 close 函数关闭连接。
2. TCP客户端的默认函数调用顺序
相对于服务器端,客户端的调用流程相对简单。创建套接字并请求连接基本上就是客户端的全部内容,其流程如下:
与服务器端相比,客户端的设计重点在于“请求连接”,即在创建客户端套接字后向服务器端发起的连接请求。服务器端调用listen函数后创建连接请求等待队列,之后客户端即可请求连接。在操作上我们常通过connect函数来完成该操作。
#include <sys/socket.h>
int connect(int sock , struct sockaddr * servaddr, socklen_t addrlen);
//成功时返回0,失败时返回1
//sock:客户端套接字文件描述符
//servaddr:保存目标服务器端地址信息的变量地址值
//addrlen:以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度。
客户端调用connect函数后,发生以下情况之一便会返回(完成函数调用)
- 服务器端接收连接请求
- 发生断网等异常情况而终端连接请求
“接收连接”并不意味着服务器端调用accept函数,而是服务器端把连接请求信息记录到等待队列。因此connect函数返回后并不立即进行数据交换。
代码回顾(Hello World客户端):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
str_len=read(sock, message, sizeof(message)-1);
if(str_len==-1)
error_handling("read() error!");
printf("Message from server: %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 第17行:创建准备连接服务器端的套接字,此时创建的是TCP套接字。
- 第21~24行:结构体变量serv_addr 中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。
- 第26行:调用 connect 函数向服务器端发送连接请求。
- 第29行:完成连接后,接收服务器端传输的数据。
- 第34行:接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。
补充:客户端套接字地址信息在哪?
服务器端的通信实现必经过程之一就是给套接字分配IP和端口号。但客户端实现过程中并没有进行套接字地址分配,而是创建套接字后调用connect函数。那么这样来说,客户端套接字是无需分配IP和端口的吗?并不是!网络数据交换必须分配IP和端口。那客户端套接字是在何时、何地以及如何分配地址的呢?
- 何时:调用connect函数时
- 何地:OS,OS的内核中
- 分配规则: IP使用主机IP地址,端口随机
客户端的IP地址和端口在调用connect函数时自动分配,无需调用bind函数进行分配。
3. 基于TCP的服务器端/客户端函数调用关系
TCP服务器端/客户端两者间并非相互独立,他们的总体流程可以概述为:服务器端创建套接字后连续调用bind,listen函数并进入等待状态,客户端通过调用connect函数发起连接请求。客户端智能等到服务器端调用listen函数后才能调用connect函数。需要明确,客户端在调用connect函数前,服务器端可能会先去调用accept函数。所以,在客户端调用connect函数期间服务器端会进入到阻塞状态,直到客户端调用完connect函数时再解除阻塞,调用accept。
两者交互过程如下图所示: