accept函数
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
被动监听客户端发起三次握手的连接请求,三次握手成功,即建立连接成功。
accept函数被动监听客户连接的过程,其实也被称为监听客户上线的过程。
对于那些只连接了一半,还未连接完成的客户,会被记录到未完成连接的队列中,队列的容量由listen函数的第二个参数(backlog)来指定。
真正用于监听的是accept函数,listen函数只是用来把套接字文件描述符从主动转变为被动。转为被动之后交给accept函数使用。accept函数用来被动监听客户端的连接。所以accept函数才是监听函数。
服务器调用accept函数监听客户连接,而客户端则调用connect函数来主动发起连接请求。
服务器端的accept函数和客户端的connect函数是对应关系。
一旦连接成功,服务器这边的TCP协议会记录客户端的IP和端口,如果是跨网通信,记录ip的就是客户所在路由器的公网Ip。
返回值
成功
返回一个通信描述符,专门用于与该连接成功的客户的通信,总之后续服务器与该客户间正式通信,使用的就是accept函数返回的“通信描述符”来实现的。
失败
返回-1,errno被设置。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockefd
已经被listen函数转为了被动描述符的“套接字文件描述符”,专门用于被动监听客户的连接。
如果sockfd没有被listen函数转为被动描述符的话,accept函数是无法将其用来监听客户连接的。
有关套接字描述符的阻塞与非阻塞问题
int socket(int domain, int type, int protocol);
服务器程序调用socket函数获得“套接字文件描述符”时,如果socket的第2个参数type没有指定SOCK_NONBLOCK的话,socket函数所返回的 “套接字文件描述符”默认就是阻塞的,所以使用accept函数来监听客户连接时,如果没有客户请求连接的话,accept函数就会阻塞,直到有客户连接为止。
如果你不想阻塞,我们就可以在调用socket函数时,给type指定SOCK_NONBLOCK宏。这个时候accept函数使用这个描述符来监听客户连接的时候,如果没有客户连接的话accept函数就不会阻塞。
在TCP/IP协议族中,只有TCP协议才有建立连接的要求,那么accept函数怎么知道你用的就是TCP协议呢?
accept函数的第一个参数是socket函数所返回的“套接字文件描述符”,它指向了socket函数所创建的套接字文件,创建套接字文件时,我们会通过参数1和参数2指定你所使用的协议。
int socket(int domain, int type, int protocol);
比如将其指定为TCP协议,所以只要你在调用socket函数创建套接字文件时有指定通信协议,accept函数就可以通过“套接字文件描述符”知道套接字文件所使用的是不是TCP协议。
因为只有tcp协议才有连接,只有当有连接的时候我们才能够去调用accept函数去等待连接。如果使用的协议不需要连接的话,调用accept函数会导致错误。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr
用于记录发起连接请求的客户端的IP和端口(port)。
前面说过建立连接时,服务器这边的TCP协议会自动记录客户端的ip和端口,如果是跨网通信的话,记录的就是客户端的公网IP。
如果服务器应用层需要用到客户ip和端口的话,可以给accept函数指定第二个参数addr,以获取TCP在连接时所自动记录客户IP和端口,如果服务器应用层不需要用的话就写NULL。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr为struct sockaddr
结构体类型,有关这个结构体,我们在前面博客已经说明过。
不过我们前面说过,虽然下层(内核)实际使用的是struct sockaddr
结构体,但是由于这个结构体用起来不方便,因此应用层会使用更加便于操作的结构体,比如使用TCP/IP协议族通信时,应用层使用的就是struct sockaddr_in
这个更加方便操作的结构体。
所以我们应该定义struct sockaddr_in
类型的addr,传递给accept函数时,将其强制转为struct sockaddr
结构体类型即可,这个过程与我们说明bind函数时的用法类似。
例子:
创建结构体类型变量
struct sockaddr_in clnaddr = {0};
获得结构体的大小
int clnsize = sizeof(clnaddr);
调用accept函数返回通信文件描述符,cfd 用于保存accept 函数调用成功所返回的通信文件描述符。
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnsize);
accept 函数的第一个参数写被转为被动的套接字文件描述符。
第二个参数将 clnaddr 强制转化为 struct sockaddr 类型。
第三个参数指定结构体变量的大小,只不过这里是取地址。
将clnaddr传递给accept函数后,accept函数会自动的将TCP记录的客户ip和端口设置到clnaddr中,我们就可以得到客户的ip和端口。
通过之前对struct sockaddr_in
成员的了解可知,clnaddr第二个成员放的是客户端的端口号,第三个成员放的是客户的IP。
struct sockaddr_in
{
sa_family_t sin_family; //设置AF_***(地址族)
__be16 sin_port; //设置端口号
struct in_addr sin_addr; //设置Ip
/* 设置IP和端口时,这个成员用不到,这个成员的作用后面再解释, */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};
addrlen
第二参数addr的大小,不过要求给的是地址。
如何给地址?
我们在下面代码中进行演示。
代码演示:调用accept函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"
void print_err(char * str,int line,int err_no)//出错处理函数
{
printf("%d,%s: %s\n",line,str,strerror(err_no));
exit(-1);
}
int main(void)
{
int ret = -1;
int sockfd = -1; //存放套接字文件描述符
/*创建使用TCP协议通信的套接字文件*/
sockfd = socket(PF_INET,SOCK_STREAM,0); //指定TCP协议
if(-1 == sockfd) //进行出错处理
print_err("socket fail",__LINE__,errno);
/*调用bind函数绑定套接字文件/ip/端口*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//指定ip格式
saddr.sin_port= htons(SPROT);//指定端口
saddr.sin_addr.s_addr = inet_addr(SIP);//设置ip
ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定
if(-1 ==ret)
print_err("bind fail",__LINE__,errno);
/*将主动的套接字文件描述符转换为被动的文件描述符,用于被动监听客户连接。*/
ret = listen(sockfd,3);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
/*调用accep函数,被动监听客户的连接*/
int cfd = 0; /*存放与客户通信的通信文件描述符*/
struct sockaddr_in clnaddr = {0};/*用来保存连接客户端的IP和端口*/
int clnaddr_size = sizeof(clnaddr);/*存放结构体变量的大小*/
cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
return 0;
}
如何使用得到的客户ip和端口
比如我这里的使用方式是打印客户端的ip和端口进行查看,此时必须调用ntohs函数和inet_ntoa函数进行端序转换。
为什么要进行端序转换?
客户的端口和ip是服务器这边的TCP协议,从客户端发送的网络数据包中提取出来的,网络数据包的端序属于网络端序,主机接收到数据后,如果你想要使用的话,就必须从网络端序转为主机端序。
ntohs函数
是htons函数功能相反的函数,ntohs函数的作用就是把客户端的端口从网络端序转为主机端序。
htons 函数我们在bind函数绑定中使用过,htons函数的作用就是把服务器的端口从主机端序转化为网络端序。
inet_ntoa函数
inet_ntoa函数功能与inet_addr函数刚好相反,inet_ntoa函数有两个功能。
· 将IPV4的32位无符号整形数的ip,从网络端序转为主机端序。
· 将实际所用的无符号整型数的ip,转成人能识别的字符串形式的ip,也就是点分十进制的形式。
inet_addr 函数的功能就是把点分十进制的ip转换为32位无符号整型的ip,然后把32位无符号整型的ip从主机端序转换为网络端序。
使用举例:
//用来存放客户端ip和端口
struct sockaddr_in clnaddr = {0};
//存放clnaddr 结构体的大小
int clnaddr_size = sizeof(clnaddr)
//调用accept函数
cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size);
当把clnaddr结构体传给accept函数之后,accept函数就会自动的把tcp协议所记录的客户端ip和端口写到clnaddr结构体中,应用层就通过结构体变量得到了客户端的ip和端口就可以直接使用,我们这里的简单使用就是打印客户端的ip和端口。
打印客户端的ip和端口:
printf("cln_port=%d, cln_addr=%s\n",ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr));
上面的printf函数里面,把clnaddr.sin_port 端口通过 ntohs 函数从网络端序转换为主机端序。然后再打印出来。把clnaddr.sin_addr 的ip地址首先从网络端序转为主机端序,然后再把转换后的主机端序ip从无符号的32位整型ip转换为字符串的点分十进制ip便于查看。
代码演示:打印客户端ip和端口
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"
void print_err(char * str,int line,int err_no)//出错处理函数
{
printf("%d,%s: %s\n",line,str,strerror(err_no));
exit(-1);
}
int main(void)
{
int ret = -1;
int sockfd = -1; //存放套接字文件描述符
/*创建使用TCP协议通信的套接字文件*/
sockfd = socket(PF_INET,SOCK_STREAM,0); //指定TCP协议
if(-1 == sockfd) //进行出错处理
print_err("socket fail",__LINE__,errno);
/*调用bind函数绑定套接字文件/ip/端口*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//指定ip格式
saddr.sin_port= htons(SPROT);//指定端口
saddr.sin_addr.s_addr = inet_addr(SIP);//设置ip
ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定
if(-1 ==ret)
print_err("bind fail",__LINE__,errno);
/*将主动的套接字文件描述符转换为被动的文件描述符,用于被动监听客户连接。*/
ret = listen(sockfd,3);
if(-1 ==ret)
print_err("listen fail",__LINE__,errno);
/*调用accep函数,被动监听客户的连接*/
int cfd = 0; /*存放与客户通信的通信文件描述符*/
struct sockaddr_in clnaddr = {0};/*用来保存连接客户端的IP和端口*/
int clnaddr_size = sizeof(clnaddr);/*存放结构体变量的大小*/
cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
if(-1 ==ret)
print_err("accept fail",__LINE__,errno);
/*打印客户端的端口和ip,一定要记得进行端口转换*/
printf("client_port = %d\n,client_ip = %s\n",ntohs(clnaddr.sin_port),inet_ntoa(clnaddr.sin_addr));
return 0;
}
不过目前,我们并没有办法来验证,因为我们还没有写客户端程序。也就没有客户端请求连接,之后把客户端程序写完之后,当客户端请求连接完成之后printf函数就会把客户端的ip和端口打印出来,进行验证。