套接字地址
IP地址是一个32bits无符号整数。网络程序将IP地址存放在IP地址结构中:
#include <netinet/in.h>
/* Internet address structure */
struct in_addr{
unsigned int s_addr; /* Network byte order (big-endian) */
};
因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byts order)(big-endian),Unix提供了以下函数用于在网络和主机字节顺序间实现转换:
#include <netinet/in.h>
// 返回按照网络字节顺序的值
unsigned long int htonl(unsigned long int hostlong); // 32bits IP地址
unsigned short int htons(unsigned short int hostshort); // 16bits 端口号
// 返回按照主机字节顺序的值
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netlong);
IP地址通常以点分十进制来表示,每个字节由它的十进制表示,并用句点和其他字节分开。因特网程序使用inet_aton
和inet_ntoa
函数实现IP地址和点分十进制串之间的转换:
注意inet_ntoa
以结构体为参数而不是指针
#include <arpa/inet.h>
// 成功返回1,失败返回0
// 点分十进制串cp --> struct in_addr *inp
int inet_aton(const char *cp, struct in_addr *inp);
// 成功返回指向点分十进制串的指针
char *inet_ntoa(struct in_addr in);
因特网定义了域名集合与IP地址集合之间的映射,这个映射通过DNS来维护。从概念上而言,DNS数据库有上百万个如下所示的主机条目结构(host entry structure)组成,其中每条定义了一组域名(一个官方名字和一组别名)和一组IP地址间的映射。
/* DNS host entry structure */
struct hostent {
char *h_name; // Official domain name of host
char **h_aliases; // Null-terminated arrar of domain names
int h_addrtype; // Host address type (AF_INET)
int h_length; // Length of an address, in bytes
char **h_addr_list; // Null-terminated array of in_addr structs
}
注意其将IP地址结构体当做字符串,我猜这样做可能是因为需要按字节大小处理IP地址
因特网应用程序通过调用gethostbyname
和gethostbyaddr
函数,从DNS数据库中检索任意的主机条目。
#include <netdb.h>
// 成功返回非NULL指针,若出错返回NULL指针,同时设置h_errno
struct hostent *gethostbyname(const char *name); // 注意 name 是域名,不是点分十进制串
struct hostent *gethostbyaddr(const char *addr, int len, 0);
从Unix程序的角度来看,套接字就是一个有相应描述符的打开文件。因特网的套接字地址存放在如下所示的类型为sockaddr_in
的16bytes结构中。
#include <netinet/in.h>
/* Internet-style socket address structure */
struct sockaddr_in{
unsigned short sin_family; // Address family (always AF_INET)
unsigned short sin_port; // Port number in network byte order
struct in_addr sin_addr; // IP address in network byte order
unsigned char sin_zero[8]; // Pad to sizeof(struct sockaddr)
}
在作为参数传入一个套接字函数时,套接字地址结构总是以引用形式(指向该结构的指针)来传递的,但不同协议族的套接字地址结构类型可能不同。函数如何声明套接字地址结构的指针类型因此成为一个问题。为了解决这个问题,定义了通用套接字地址结构sockaddr
:
#include <sys/socket.h>
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr{
unsigned short sa_family; // Protocol family
char sa_data[14]; // Address data
}
在ANSI C出现之后,可以定义通用的指针类型void *
,但套接字函数是在ANSI C之前定义的
套接字接口
以下为一对TCP客户与服务器进行之间进行交互的典型流程:
Client process Server process
socket socket
| |
| |
| v
| bind
| |
| |
| v
| listen
| |
| |<-----------------------------------------------+
| v ^
| accept |
| | |
| v |
| 一直阻塞到 |
| 客户端连接到达 |
| | |
v Connection request | |
connect ------------------------->+ |
| (TCP三路握手) | |
| | |
v Client sends requset v Server processes request ____________ |
write ========================> read ==========================> | Resource | |
| | and requset source |__________| |
| | |__________| |
| | |__________| |
| | // |
| | +====================+ |
v Server sends respense v // get the source file |
read <======================== write <==+ |
| | |
| | |
v EOF v |
close ------------------------> read |
| |
v Await connection request from next client |
close ------------------------------------------->+
创建套接字
客户端和服务器使用socket
函数来创建一个套接字描述符(socket descriptor)。
#include <sys/socket.h>
// 成功返回非负描述符,失败返回-1
int socket(int family, int type, int protocol);
/*
* 创建一个套接字描述符
* AF_INET 表明我们正在使用IPv4协议
* SOCK_STREAM 表明这个套接字是字节流套接字(TCP)
* protocol是0表示选择给定family和type组合的系统默认值
*/
socketfd = socket(AF_INET, SOCK_STREAM, 0);
socket
返回的套接字描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决于我们是客户端还是服务器。
connect(客户端)
客户端通过调用connect
函数来建立和服务器的连接:
#include <sys/socket.h>
// 若成功返回0,出错返回-1
int connect(int sockfd, const struct sockaddr *serv_addr, int addrlen);
connect
函数试图与套接字地址为serv_addr
的服务器建立一个因特网连接,其中addrlen
是sizeof(sockaddr_in)
。connect
函数仅在连接成功建立或是发生错误时才返回。如果成功,sockfd
描述符现在就准备好读写了,即使用Unix I/O函数与服务器通信。
当connect
失败则该套接字不可再用,必须关闭,不能对这样的套接字再次调用connect
函数。
包装socket
和connect
为open_clientfd
:
/*
* 创建socket描述符并与运行在hostname上的服务器(知名端口)建立一个连接
* 若成功则返回一个打开的套接字描述符
* 若Unix出错则返回-1
* 若DNS出错则返回-2
*/
typedef struct sockaddr SA; // Generic socket address structure
int open_clienfd(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(sockaddr_in)) < 0) {
return -1;
}
return clientfd;
}
bind、listen、accept(服务器)
服务器使用bind
、listen
、accept
和客户端建立连接。
#include <sys/socket.h>
// 成功返回0,失败返回-1
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
int listen(int sockfd, int backlog);
bind
函数告诉内核将my_addr
中的服务器套接字地址与套接字描述符sockfd
联系起来,参数addrlen
是sizeof(sockaddr_in)
。
listen
函数将sockfd
从一个主动套接字转换为一个监听套接字(listening socket),该套接字可以接受来自客户端的连接请求。backlog
参数规定了内核应该为相应套接字排队的最大连接个数。
包装socket
、bind
、listen
为open_listenfd
:
/*
* 打开和返回一个监听描述符,这个描述符准备好在知名端口port上接收连接请求
* 若成功返回描述符
* 若Unix出错返回-1
*/
#define LISTENQ 1024
typedef struct sockaddr SA; // Generic socket address structure
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
* 使服务器可以立即终止和重启,默认地,一个重启的服务器将
* 在大约30秒内拒绝客户端的连接请求
*/
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval, sizeof(int)) < 0) {
return -1;
}
/* Listenfd will be an end point for all requsts to port
on any IP address for this host */
bzero((char *)&serveraddr, sizeof(serveraddr);
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.saddr = htonl(INADDR_ANY); // INADDR_ANY表示通配地址,即让内核选择IP地址(wildcard)
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;
}
服务器通过调用accept
函数来等待来自客户端的连接请求。
#include <sys/socket.h>>
// 成功则返回非负描述符,出错返回-1
int accept(int listenfd, struct sockaddr *cliaddr, int *addrlen);
accept
函数等待来自客户端的连接请求到达监听描述符listenfd
,然后在cliaddr
中填写客户端的套接字地址,并返回一个已连接描述符(connected desctptor)(或者说:从已完成连接队列头返回下一个已完成连接)。这个描述符可被用来利用Unix I/O函数与客户端通信。
cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。addrlen为值-结果参数:调用前,我们将由addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
accept
返回的描述符为内核自动生成的一个全新描述符,而不是listenfd。一个服务器通常仅仅创建一个监听描述符,它在服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。
关闭套接字
通过调用Unix close
关闭(连接)描述符:
#include <unisted.h>
// 若成功返回0,若出错则返回-1
int close(int sockfd);
- older
- Newer