一个完整的Socket则用一个相关描述:
{协议,本地地址,本地端口,远程地址,远程端口}。
每 一个Socket有一个本地的唯一Socket号,由操作系统分配。套接字有3种类型:流式套接字(SOCK_STREAM)、数据包套接字 (SOCK_DGRAM)和原始套接字。流式套接字可以提供可靠的、面向连接的通信流。如果通过流式套接字发送了顺序的数据:1、2。那么数据到达远程时 候的顺序也是1、2。流式套接字可用于Telnet远程连接、WWW服务等需要使数据顺序传递的应用,它使用TCP协议保证数据传输的可靠性。流式套接字 的工作原理如图18.9所示,我们将网络中的两台主机分别作为服务器和客户机看待。
数据包套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。数据包套接字使用者数据包协议UDP,数据只是简单地传送到对方。数据包套接字的工作原理如图18.10所示。
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
18.3.2 创建套接字
套接字是通过标准的UNIX文件描述符和其他的程序通信的一个方法。套接字在使用前必须先被建立,建立套接字的系统调用为socket(),它的一般形式是:
int socket(int domain, int type, int protocol);
创建出来的套接字是一条通信线路的一个端点,domain参数负责指定地址族,type参数负责指定与这个套接字一起使用的通信类型,而protocol参数负责制定所使用的协议。domain参数的取值范围如表18.2所示。
表18.2 domain参数的取值范围
参 数 | 说 明 |
AF_UNIX | UNIX内部(文件系统套接字) |
AF_INET | ARPA因特网协议(UNIX网络套接字) |
AF_ISO | ISO标准协议 |
AF_NS | 施乐网络系统协议 |
AF_IPX | NOVELL IPX协议 |
AF_APPLETALK | Appletalk DDS |
最常用的套接字域是AF_UNIX 和AF_INET,前者用于通过UNIX文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用在穿过包括Internet在内的各种TCP/IP网络而进行通信的应用程序中。
套 接字参数type指定了与新套接字对应的通信特性。它的取值范围为枚举常量SOCK_STREAM和SOCK_DGRAM。SOCK_STREAM是一个 有序的、可靠的、基于连接的双向字节流。对于一个AF_INET域的套接字来说,如果在恋歌流式套接字的两端之间建立的是一个TCP连接,连接时默认值即 为该特性。SOCK_DGRAM是一个数据图服务,可以用来发送最大长度是一个固定值的消息,但消息是否会被送达或者消息的先后次序是否会在网络传输中被 重新安排并没有保证。对于AF_INET域的套接字来说,这种类型的通信是由UDP提供的。
通信所用的协议通常是由套接字的类型和套接字的域来决定,如果还有其他的协议可以选择,那么就在protocol参数里设置。protocol参数默认值为0,表示使用默认的协议。
socket系统调用返回的是一个描述符,它与文件描述符非常相似。当这个套接字和通信线路另一端的套接字连接好以后,就可以进行数据的传输和接收操作了。
18.3.3 套接字地址
每个套接字域都有独特的地址格式。对于一个AF_UNIX 套接字来说,它的地址是由一个包含在sys/un.h头文件里的sockaddr_un结构描述的。该结构的定义为:
- struct sockaddr_un {
- sa_family_t sun_family; // AF_UNIX
- char sun_path[]; // 路径
- };
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[]; // 路径
};
因 为不同类型的地址都需要传递到对套接字进程处理的系统调用里去,所以定义各种地址格式时使用的结构也都很相似,每个结构的开始都是一个定义地址类型(即套 接字域)的数据项。sun_family_t是由X/Open技术规范定义的,在Linux系统上,它被声明为一个short类型。sun_path给出 的路径长度是有限制的,Linux规定其最长不能超过108个字符。因为地址结构在长度方面是不固定的,所以许多套接字调用都要用到或输出一个用来复制特 定地址结构的长度值。
AF_INET域里的套接字地址是由一个定义在netinet/in.h头文件里的sockaddr_in结构确定的。该结构的定义为:
struct sockaddr_in {
short int sin_family; // AF_INET
unsigned short int sin_port; // 端口号
struct in_addr sin_addr; // Internet地址
};
其中Internet地址是netinet/in.h头文件中定义的另一个结构体,该结构体的定义为:
struct in_addr {
unsigned long int s_addr;
};
一个AF_INET套接字完全可以由它的域、IP地址和端口号确定下来。从应用程序的角度看,各种套接字的行为就像是文件描述符,用一个独一无二的整数就可以把它们表示出来。
18.3.4 套接字的名字
要使socket()调用创建的套接字能够被其他进程使用,程序就必须给该套接字起个名字。AF_UNIX 套接字会关联到一个文件系统的路径名上去,AF_INET套接字将关联到一个IP端口号上去。为套接字命名可使用bind()系统调用,它的一般形式如下:
int bind(int socket, const struct sockaddr * address, size_t address_len);
bind() 系统调用的作用是把参数address中给出的地址赋值给与文件描述符socket相关联的未命名套接字。地址结构的长度是通过address_len参 数传递的。地址的长度和类型取决于地址族。bind()调用需要用一个与之对应的地址结构指针指向真正的地址类型。该调用成功时将返回0,否则返回–1, 并将errno变量设置为表18.3中的值。
表18.3 bind()系统调用返回的错误代码
代 码 | 说 明 |
EBADF | 文件描述符无效 |
ENOTSOCK | 该文件描述符代表的不是一个套接字 |
EINVAL | 该文件描述符是一个已命名套接字 |
EADDRNOTAVAIL | 地址不可用 |
EADDRINUSE | 该地址已经绑定一个套接字 |
AF_UNIX 套接字对应的错误代码比上表要多出两个,分别是EACCESS,表示权限不足,不能创建文件系统中使用的名字;ENOTDIR/ENAMETOOLONG,表示路径错误或路径名太长。
18.3.5 创建套接字队列
为了能够在套接字上接受接入的连接,服务器程序必须创建一个队列来保存到达的请求。创建队列可使用系统调用listen()完成,它的一般形式为:
int listen(int socket, int backlog);
Linux 系统可能会对队列里能够容纳的排队连接的最大个数有限制。在这个最大值的范围内,listen()将把队列长度设置为backlog个连接。在套接字上排 队的接入连接个数最多不能超过这个数字,再往后的连接将被拒绝,用户的连接请求将会失败。这是listen()提供的一个机制,在服务器程序紧张地处理着 上一个客户的时候,后来的连接将被放到队列里排队等号。backlog常用的值是5。
listen()函数成功时会返回0,否则返回–1,它的错误代码包括EBADF、EINVAL和ENOTSOCK,含义同bind()系统调用的错误代码相同。
18.3.6 接受连接
服务器上的应用程序创建好命名套接字之后,就可以通过accept()系统调用来等待客户端程序建立对该套接字的连接了。accept()的一般形式是:
int accept(int socket, struct sockaddr * address, size_t * address_len);
accept() 系统调用会等到有客户程序试图连接到由socket参数指定的套接字时才返回。该客户就是套接字队列里排在第一位的连接。accept()函数将创建出一 个新的套接字来与该客户进行通信,返回的是与之对应的文件描述符。新套接字的类型与服务器监听套接字的类型相同。
套接字必须是被bind()调用命名过的,并且还要有一个由listen()调用分配的连接队列。客户的地址将被放在address指向的sockaddr结构里。如果不关心客户的地址,可以在这里使用一个空指针。
参数address_len给出了客户结构的长度。如果客户地址的长度超过了这个值,就会被截短。在调用accept()之前,必须把address_len设置为预期的地址长度。当这个调用返回时,address_len将被设置为客户的地址结构的实际长度。
如果套接字队列里没有排队等候的连接,accept将阻塞程序,直到有客户建立连接为止。这个行为可以用O_NONBLOCK标志改变,方法是对这个套接字文件描述符调用fcntl()函数。代码如下:
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, O_NONBLOCK|flags);
如 果有排队等候的客户连接,accept()函数将返回一个新的套接字文件描述符,否则它将返回–1。其错误原因除类似于bind()调用和 listen()调用中的情况之外,还有一个EWOULDBLOCK,如果前面指定了O_NONBLOCK标志,但队列里没有排队的连接,就会出现这个错 误。如果进程阻塞在accept()调用里的时候执行被中断了,就会出现EINTR错误。
18.3.7 请求连接
当客户想要连接到服务器的时候,它会尝试在一个未命名套接字和服务器的监听套接字之间建立一个连接。它们用connect()系统调用来完成这一工作,它的一般形式是:
int connect(int socket, const struct sockaddr * address, size_t address_len);
参 数socket指定的套接字将连接到参数address指定的服务器套接字上去,服务器套接字的长度由参数address_len指定。套接字必须是通过 socket调用获得的一个有效的文件描述符。如果操作成功,函数返回0,否则返回–1。该函数产生的错误代码如表18.4所示。
表18.4 connect()系统调用返回的错误代码
代 码 | 说 明 |
EBADF | 文件描述符无效 |
EALREADY | 套接字上已经有了一个正在使用的连接 |
ETIMEDOUT | 连接超时 |
ECONNREFUSED | 连接请求被服务器拒绝 |
如 果连接不能立刻建立起来,connect()会阻塞一段不确定的倒计时时间,这段倒计时时间结束后,这次连接就会失败。如果connect()调用是被一 个信号中断的,而这个信号又得到了处理,connect还是会失败,但这次连接尝试是成功的,它会以异步方式继续尝试。
类 似于accept()调用,connect()的阻塞特性可以用设置该文件描述符的O_NONBLOCK标志的办法来改变。在这种情况下,如果连接不能立 刻建立起来,connect()会失败并把errno变量设置为EINPROGRESS,而连接将以异步方式继续尝试。
异步连接的处理是比较困难的,而我们可以在套接字文件描述符上用一个select()调用来表明该套接字已经处于写就绪状态。
18.3.8 关闭连接
系 统调用close()函数可以结束服务器和客户上的套接字连接,就像对底层文件描述符进行操作一样。要想关闭套接字,就必须把服务器和客户两头都关掉才 行。对服务器来说,应该在read()返回0时进行该操作,但如果套接字是一个面向连接的类型并且设置了SOCK_LINGER选项,close()调用 会在该套接字尚有未传输数据时阻塞。
18.3.9 套接字通信
本节将设计两个例子演示套接字通信的过程,其中一个为服务器程序,另一个为客户程序。
1.服务器程序
服务器程序的代码如下:
- #include <sys/types.h>
- #include <sys/socket.h> // 包含套接字函数库
- #include <stdio.h>
- #include <netinet/in.h> // 包含AF_INET相关结构
- #include <arpa/inet.h> // 包含AF_INET相关操作的函数
- #include <unistd.h>
- int main()
- {
- int server_sockfd, client_sockfd; // 用于保存服务器和客户套接字标识符
- int server_len, client_len; // 用于保存服务器和客户消息长度
- struct sockaddr_in server_address; // 定义服务器套接字地址
- struct sockaddr_in client_address; // 定义客户套接字地址
- server_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 定义套接字类型
- server_address.sin_family = AF_INET; // 定义套接字地址中的域
- server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
- // 定义套接字地址
- server_address.sin_port = 9734; // 定义套接字端口
- server_len = sizeof(server_address);
- bind(server_sockfd, (struct sockaddr *) &server_address, server_len); // 定义套接字名字
- listen(server_sockfd, 5); // 创建套接字队列
- while (1) {
- char ch;
- printf("服务器等待消息/n");
- client_len = sizeof(client_address);
- client_sockfd = accept(server_sockfd, // 接收连接
- (struct sockaddr *) &client_address,
- (socklen_t *__restrict) &client_len);
- read(client_sockfd, &ch, 1); // 读取客户消息
- ch++;
- write(client_sockfd, &ch, 1); // 向客户传送消息
- close(client_sockfd); // 关闭连接
- }
- }
- 该程序监听本地的9734端口,程序运行后等待客户通过该端口连接,从客户传送的消息里读取一个字符,然后对该字符进行加1操作后,再传送给客户,并关闭该连接。
- 2.客户程序
- 客户程序的源代码如下:
- #include <sys/types.h>
- #include <sys/socket.h> // 包含套接字函数库
- #include <stdio.h>
- #include <netinet/in.h> // 包含AF_INET相关结构
- #include <arpa/inet.h> // 包含AF_INET相关操作的函数
- #include <unistd.h>
- int main() {
- int sockfd; // 用于保存客户套接字标识符
- int len; // 用于保存客户消息长度
- struct sockaddr_in address; // 定义客户套接字地址
- int result;
- char ch = 'A'; // 定义要传送的消息
- sockfd = socket(AF_INET,SOCK_STREAM, 0); // 定义套接字类型
- address.sin_family = AF_INET; // 定义套接字地址中的域
- address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定义套接字地址
- address.sin_port = 9734; // 定义套接字端口
- len = sizeof(address);
- result = connect(sockfd, (struct sockaddr *) &address, len);
- // 请求连接
- if (result == -1) {
- perror("连接失败");
- return 1;
- }
- write(sockfd, &ch, 1); // 向服务器传送消息
- read(sockfd, &ch, 1); // 从服务器接收消息
- printf("来自服务器的消息是%c/n", ch);
- close(sockfd); // 关闭连接
- return 0;
- }
客户端程序向本地的9734端口请求连接,如果连接成功即发送一个字符A作为消息。然后从服务器传送的消息中读取一个字符,并将该字符输出,退出程序。
将 这两个程序分别编译,然后打开两个终端,第一个终端运行服务器程序,这时会出现提示符“服务器等待消息”。第二个终端运行客户程序,客户程序会将字符A作 为消息传送给服务器程序,然后服务器程序对该字符进行加1处理,传送回客户程序。客户程序的输出是“来自服务器的消息是B”。这样两个程序就完成了连接和 通信。结束客户端程序可使用组合键Ctrl + C。
struct sockaddr{
u_char sa_1en; /* 总长度 */
u_char sa_family; /* 地址簇 */
char sa_data[14]; /* 实际长度,地址值 */
};
typedef struct sockaddr S0CKADDR;
第一个字段是整个结构的大小。
第二个字段是地址族,指出通信域的地址表示格式,对于Internet域的地址族为AF_INET,一般常用的常量有:
AF_LOCAL: 本地主机(类似管道)。
AF_UNIX: 同AF_LOCAL,该常量的定义是为了以前兼容。
AF_INET: Internet地址,用于TGP, UDP等。
AF_NS: XEROX网络系统地址。
AF_ISO; ISO协议地址。
AF_OSI: 同上。
AF_IPX: Novell网络地址。
AF_DECnet: DEC网络地址。
AF_APPLETALK: Apple Talk网络地址。
AF_ROUTE: 内部路由协议地址。
第三个字段为实际地址。
对于Internet域的地址(AF_INET)表示,我们常用的是sockaddr_in结构,它定义在头文样<netinet/in.h>中,该结构如下所示:
struct sockaddr_in{
u_char sin_len;
u_char sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
该结构的前两个字段与sockaddr结构中的含义相同,第三个字段是远程要连接的服务端的端口。第四个字段是一个存放IP地址的结构,该结构如下:
struct in_addr {
u_long s_addr;
};
struct sockaddr和struct sockaddr_in可用强制类型转换。
对于IPv4的IP地址,刚好是四个字节,存放在一个无符号整型变量中。例如一个IP地址通常的写法是162.27.8.9$,在存放时按整型存放。在存放时,不要简单的将它按四个字节从高到低存在int类型的s_addr中,因为整型变量的字节存放是高位在前还是低位在前依赖于机器及系统的实现,为了程序的可移植性,最好使用下面的宏定义函数进行格式转换,以保证程序的可移植性。
htonl();
htons();
ntohl();
ntohs();
其中htonl函数将长整型的主机字节顺序的hostlong转换为网络字节顺序,htons函数将short int类型的hostshort转换为网络字节顺序。其余两个函数刚好相反,可以从函数名看出各自的功能。也可以由函数inet_addr函数将字符串形式的以点分隔的IP地址转换为四字节的地址,或由gethostbyname, gethostbyaddr函数得到hostent结构的地址