监听套接字与已连接套接字
监听套接字(listening socket)和已连接套接字(connected socket)之间的区别常会使很多人感到迷惑。本文简要描述一下这两者的区别。为了说明监听套接字与已连接套接字的区别,我们先来看一下套接字在连接中的含义。
从内核的角度来看,一个套接字就是通信的一个端点。一个连接由它两端的套接了地址唯一确定,这对套接字地址叫做套接字对(socket pair),由下列4元组来表示:
(clientip:clientport, serverip:serverport)
其中,clientip 是客户端的IP地址,clientport 是客户端的端口,serverip 是服务器的IP地址,而 serverport 是服务器的端口。
上图展示了一个套接字对4元组,即一个客户端与一个服务器之间的连接。在这个示例中,客户端套接字为
128.2.194.242:51234
服务器套接字地址为
114.113.200.133:80
给定客户端和服务器地址,客户和服务器之间的连接就由下列套接字对唯一确定了:
(128.2.194.242:51234, 114.113.200.133:80)
在上面的例子中,客户端是发起连接请求的主动实体,服务器是等待来自客户端连接请求的被动实体。我们知道,socket函数可以创建一个套接字。默认情况,内核会认为socket函数创建的套接字是主动套接字(active socket),它存在于一个连接的客户端。而服务器调用listen函数告诉内核,该套接字是被服务器而不是客户端使用的,即listen函数将一个主动套接字转化为监听套接字(下文以 listenfd 表示)。监听套接字可以接受来自客户端的连接请求。
服务器通过accept函数等待来自客户端的连接请求到达监听套接字 listenfd,并返回一个已连接套接字(下文以 connfd 表示)。利用 I/O 函数,这个 connfd 可以被用来与客户端进行通信。
上面就是监听套接字与已连接套接字的基本区别了。具体来说,监听套接字,是服务器作为客户端连接请求的一个端点,它被创建一次,并存在于服务器的整个生命周期。已连接套接字是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一次已连接套接字,它只存在于服务器为一个客户端服务的过程中。
值得指出的是,无论是监听套接字,还是已连接套接字,都是只存在于服务器端。
上图描绘了监听套接字和已连接套接字的角色。在第一步中,服务器调用accept,等待连接请求到达监听套接字 listenfd,假设该监听套接字的文字描述符为3(0,1,2已预留给标准文件使用)。在第二步中,客户端调用connect函数,发送一个连接请求到 listenfd。第三步,accept函数打开一个新的已连接套接字 connfd (假设套接字的文件描述符为4),在 clientfd 和 connfd 之间建立连接,并且随后返回给服务器应用程序。客户端也从connect函数返回。此时,客户端和服务器就可以分别通过读写 clientfd 和 connfd 来回传送数据了。
下面看一段 C语言实现的 echo 程序,来自《深入理解计算机系统》
/********************************
* Client/server helper functions
********************************/
/*
* open_clientfd - open connection to server at
* and return a socket descriptor ready for reading and writing.
* Returns -1 and sets errno on Unix error.
* Returns -2 and sets h_errno on DNS (gethostbyname) error.
*/
/* $begin open_clientfd */
int open_clientfd(char *hostname, int port) {
int clientfd;
struct hostent *hp;
struct sockaddr_in serveraddr;
if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1; /* Check errno for cause of error */
/* Fill in the server's IP address and port */
if ((hp = gethostbyname(hostname)) == NULL)
return -2; /* Check h_errno for cause of error */
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
bcopy((char *) hp->h_addr_list[0],
(char *) &serveraddr.sin_addr.s_addr, hp->h_length);
serveraddr.sin_port = htons(port);
/* Establish a connection with the server */
if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
return -1;
return clientfd;
}
/* $end open_clientfd */
/*
* open_listenfd - open and return a listening socket on port
* Returns -1 and sets errno on Unix error.
*/
/* $begin open_listenfd */
int open_listenfd(int port) {
int listenfd, optval = 1;
struct sockaddr_in serveraddr;
/* Create a socket descriptor */
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
/* Eliminates "Address already in use" error from bind */
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *) &optval, sizeof(int)) < 0)
return -1;
/* Listenfd will be an endpoint for all requests to port
on any IP address for this host */
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons((unsigned short) port);
if (bind(listenfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0)
return -1;
return listenfd;
}
/* $end open_listenfd */
其中 三个 函数bind,listen,和accept 被服务器用来和客户端建立连接。
int socket(int domain, int type, int protocol);
这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。
如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值。bind()把用addr指定的地址赋值给用文件描述符代表的套接字sockfd。
addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。
通常,在一个SOCK_STREAM套接字接收连接之前,必须通过bind()函数用本地地址为套接字命名。
备注:
调用bind()函数之后,为socket()函数创建的套接字关联一个相应地址,发送到这个地址的数据可以通过该套接字读取与使用。
备注:
bind()函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。
如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind()函数,同时也避免不必要的复杂度。
在一般情况下,对于服务器进程问题需要调用bind()函数,对于客户进程则不需要调用bind()函数。
int listen(int sockfd, int backlog);
listen函数使用主动连接套接口变为被动连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
在TCP服务器编程中listen函数把进程变为一个服务器进程,并指定相应的套接字变为被动连接。
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。
它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。
新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。
总结:
-流式套接字步骤:
- 服务器和客户端分别调用socket()创建套接字
- 服务器和客户端分别调用bind()函数绑定服务器地址
- 服务器调用listen()监听, 客户机调用connect()进行连接并向服务器发出连接请求.
- 服务器调用accept()接受请求并重新创建一个套接字用于和客户机之间通信连接.
- 调用send() 和recv() 收发数据.
- 任何一方可调用close()关闭
============END============