套接字是一个双向通信设备,可用于同一台主机上不同进程之间的通信,也可用于沟通
位于不同主机的进程。套接字是本章中介绍的所有进程间通信方法中唯一允许跨主机通信的
方式。Internet 程序,如Te l n e t 、rlogin 、FTP 、talk 和万维网都是基于套接字的。
例如,你可以用一个Te l n e t 程序从一台网页服务器获取一个万维网网页,因为它们都使
用套接字作为网络通信方式
。可以通过执行telnet www.codesourcery.com 80 连接到
位于www.codesourcery.com 主机的网页服务器。魔数80 指明了连接的目标进程是运行于
www.codesourcery.com 的网页服务器而不是其它什么进程。成功建立连接后,试着输入
GET / 。这会通过套接字发送一条消息给网页服务器,而相应的回答则是服务器将主页的
HTML 代码传回然后关闭连接——例如:
% telnet www.codesourcery.com 80
Trying 206.168.99.1...
Connected to merlin.codesourcery.com (206.168.99.1).
Escape character is '^]'.
GET /
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=iso-8859-1">
...
5.5.1 套接字概念
当你创建一个套接字的时候你需要指定三个参数:通信类型,命名空间和协议。
通信类型决定了套接字如何对待被传输的数据,同时指定了参与传输过程的进程数量。
当数据通过套接字发送的时候会被分割成段落,这些段落分别被称为一个包(packet )。通
信类型决定了处理这些包的方式,以及为这些包定位目标地址的方式。
· 连接式(Connection style )通信保证所有包都以发出时的顺序被送达。如果由于网
络的关系出现包丢失或顺序错乱,接收端会自动要求发送端重新传输。
连接类型的套接字可想象成电话:发送和接收端的地址在开始时连接被建立的时候
都被确定下来。
· 数据报式(Datagram style)的通信不确保信息被送到,也不保证送到的顺序。数
据可能由于网络问题或其它情况在传输过程中丢失或重新排序。每个数据包都必须
标记它的目标地址,而且不会被保证送到。系统仅保证“尽力”做到,因此数据包
可能消失,或以与发出时不同的顺序被送达。
数据报类型的通信更类似邮政信件。发送者为每个单独信息标记收信人地址。
套接字的命名空间指明了套接字地址的书写方式。套接字地址用于标识一个套接字连接
的一个端点。例如,在“本地命名空间”中的套接字地址是一个普通文件。而在“Internet
命名空间”中套接字地址由网络上的一台主机的Internet 地址(也被称为 Internet 协议地址
或IP 地址)和端口号组成。端口号用于区分同一台主机上的不同套接字。
协议指明了数据传输的方式。常见的协议有如下几种:TCP/IP,Internet 上使用的最主
要的通信协议;AppleTalk 网络协议;UNIX 本地通信协议等。通信类型、命名空间和协议
三者的各种组合中,只有部分是有效的。
5.5.2 系统调用
套接字比之前介绍的任何一种进程间通信方法都更具弹性。这里列举了与套接字相关的
系统调用:
socket——创建一个套接字
close ——销毁一个套接字
connect——在两个套接字之间创建连接
bind——将一个服务器套接字绑定一个地址
listen——设置一个套接字为接受连接状态
accept——接受一个连接请求并为新建立的连接创建一个新的套接字
套接字通常被表示为文件描述符。
创建和销毁套接字
Socket 和close 函数分别用于创建和销毁套接字。当你创建一个套接字的时候,需指
明三种选项:命名空间,通信类型和协议。利用PF_ 开头(标识协议族,protocol families )
的常量指明命名空间类型。例如,PF_LOCAL 或PF_UNIX 用于标识本地命名空间,而
PF_INET表示 Internet 命名空间。用以SOCK_开头的常量指明通信类型。SOCK_STREAM
表示连接类型的套接字,而SOCK_DGRAM表示数据报类型的套接字。
第三个参数,协议,指明了发送和接收数据的底层机制。每个协议仅对一种命名空间和
通信类型的组合有效。因为通常来说,对于某种组合都有一个最合适的协议,为这个参数指
定0 通常是最合适的选择。如果socket 调用成功则会返回一个表示这个套接字的文件描述
符。与操作普通文件描述符一样,你可以通过read 和write 对这个套接字进行读写。当
你不再需要它的时候,应调用close 删除这个套接字。
调用connect
要在两个套接字之间建立一个连接,客户端需指定要连接到的服务器套接字地址,然后
调用connect。客户端指的是初始化连接的进程,而服务端指的是等待连接的进程。客户
端调用connect以在本地套接字和第二个参数指明的服务端套接字之间初始化一个连接。
第三个参数是第二个参数中传递的标识地址的结构的长度,以字节计。套接字地址格式随套
接字命名空间的不同而不同。
发送信息
所有用于读写文件描述符的技巧均可用于读写套接字。关于 Linux 底层 I/O 函数及一些
相关使用问题的讨论请参考附录B 。而专门用于操作套接字的 send 函数提供了write 之外
的另一种选择,它提供了write 所不具有的一些特殊选项;参考send 的手册页以获取更
多信息。
5.5.3 服务器
服务器的生命周期可以这样描述:创建一个连接类型的套接字,绑定一个地址,调用
listen 将套接字置为监听状态,调用accept 接受连接,最后关闭套接字。数据不是直接
经由服务套接字被读写的;每次当程序接受一个连接的时候,Linux 会单独创建一个套接字
用于在这个连接中传输数据。在本节中,我们将介绍bind、listen 和accept。
要想让客户端找到,必须用bind 将一个地址绑定到服务端套接字。Bind 函数的第一
个参数是套接字文件描述符。第二个参数是一个指针,它指向表示套接字地址的结构。它的
格式取决于地址族。第三个参数是地址结构的长度,以字节计。将一个地址绑定到一个连接
类型的套接字之后,必须通过调用 listen 将这个套接字标识为服务端。Listen 的第一个
参数是套接字文件描述符。第二个参数指明最多可以有多少个套接字处于排队状态。当等待
连接的套接字超过这个限度的时候,新的连接将被拒绝。它不是限制一个服务器可以接受的
连接总数;它限制的是被接受之前允许尝试连接服务器的客户端总数。
服务端通过调用accept 接受一个客户端连接。第一个参数是套接字文件描述符。第二
个参数是一个指向套接字地址结构的指针;接受连接后,客户端地址将被写入这个指针指向
的结构中。第三个参数是套接字地址结构体的长度,以字节计。服务端可以通过客户端地址
确定是否希望与客户端通信。调用 accept 会创建一个用于与客户端通信的新套接字,并返
回对应的文件描述符。原先的服务端套接字继续保持监听连接的状态。用 recv 函数可以从
套接字中读取信息而不将这些信息从输入序列中删除。它在接受与 read 相同的一组参数的
基础上增添了一个FLAGS参数。指定 FLAGS为MSG_PEEK可以使被读取的数据仍保留
在输入序列中。
5.5.4 本地套接字
要通过套接字连接同一台主机上的进程,可以使用符号常量PF_LOCAL 和PF_UNIX
所代表的本地命名空间。它们被称为本地套接字(local sockets )或者UNIX 域套接字
(UNIX-domain sockets )。它们的套接字地址用文件名表示,且只在建立连接的时候使用。
套接字的名字在struct sockaddr_un 结构中指定。你必须将 sun_family 字段设置
为AF_LOCAL以表明它使用本地命名空间。该结构中的sun_path 字段指定了套接字使用
的路径,该路径长度必须不超过108 字节。而struct sockaddr_un 的实际长度应由
SUN_LENG 宏计算得到。可以使用任何文件名作为套接字路径,但是进程必须对所指定的
目录具有写权限,以便向目录中添加文件。如果一个进程要连接到一个本地套接字,则必须
具有该套接字的读权限。尽管多台主机可能共享一个文件系统,只有同一台主机上运行的程
序之间可以通过本地套接字通信。
本地命名空间的唯一可选协议是0 。
因为它存在于文件系统中,本地套接字可以作为一个文件被列举。如下面的例子,注意
开头的s:
% ls -l /tmp/socket
srwxrwx--x 1 user group 0 Nov 13 19:18 /tmp/socket
当结束使用的时候,调用unlink 删除本地套接字。
5.5.5 使用本地套接字的示例程序
我们用两个程序展示套接字的使用。列表5.10 中的服务器程序建立一个本地命名空间
套接字并通过它监听连接。当它连接之后,服务器程序不断从中读取文本信息并输出这些信
息直到连接关闭。如果其中一条信息是“quit”,服务器程序将删除套接字,然后退出。服
务器程序socket-server 接受一个标识套接字路径的命令行参数。
代码列表 5.10 (socket-server.c)本地命名空间套接字服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* 不断从套接字读取并输出文本信息直到套接字关闭。当客户端发送“quit”消息的
时候返回非 0 值,否则返回 0。*/
int server (int client_socket)
{
while (1) {
int length;
char* text;
/* 首先,从套接字中获取消息的长度。如果 read 返回 0 则说明客户端关闭了连
接。*/
if (read (client_socket, &length, sizeof (length)) == 0)
return 0;
/* 分配用于保存信息的缓冲区。*/
text = (char*) malloc (length);
/* 读取并输出信息。*/
read (client_socket, text, length);
printf (“%s\n”, text);
/* 如果客户消息是“quit”,我们的任务就此结束。*/
if (!strcmp (text, “quit”)) {
/* 释放缓冲区。*/
free (text);
return 1;
}
/* 释放缓冲区。*/
free (text);
/* 译者注:合并了勘误中的提示,并增加此返回语句。*/
return 0;
}
}
int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
int socket_fd;
struct sockaddr_un name; //套接字名称
int client_sent_quit_message;
/* 创建套接字。*/
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0); //PF_LOCAL 本地命名空间 连接型套接字
/* 指明这是服务器。*/
name.sun_family = AF_LOCAL;//你必须将 sun_family 字段设置为AF_LOCAL以表明它使用本地命名空间。
strcpy (name.sun_path, socket_name);
bind (socket_fd, &name, SUN_LEN (&name));
/* 监听连接。*/
listen (socket_fd, 5); //
/*通过调用 listen 将这个套接字标识为服务端。Listen 的第一个
参数是套接字文件描述符。第二个参数指明最多可以有多少个套接字处于排队状态。当等待
连接的套接字超过这个限度的时候,新的连接将被拒绝。它不是限制一个服务器可以接受的
连接总数;它限制的是被接受之前允许尝试连接服务器的客户端总数。*/
/* 不断接受连接,每次都调用 server() 处理客户连接。直到客户发送“quit”消
息的时候退出。*/
do {
struct sockaddr_un client_name;
socklen_t client_name_len;
int client_socket_fd;
/* 接受连接。*/
client_socket_fd = accept (socket_fd, &client_name, &client_name_len);
/* 处理连接。*/
client_sent_quit_message = server (client_socket_fd);
/* 关闭服务器端连接到客户端的套接字。*/
close (client_socket_fd);
}
while (!client_sent_quit_message);
/* 删除套接字文件。*/
close (socket_fd);
unlink (socket_name);
return 0;
}
列表 5.11 中的客户端程序将连接到一个本地套接字并发送一条文本消息。本地套接字
的路径和要发送的消息由命令行参数指定。
代码列表 5.11 (socket-client.c )本地命名空间套接字客户端
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* 将 TEXT 的内容通过 SOCKET_FD 代表的套接字发送。*/
void write_text (int socket_fd, const char* text)
{
/* 输出字符串(包含结尾的 NUL 字符)的长度。*/
int length = strlen (text) + 1;
write (socket_fd, &length, sizeof (length));
/* 输出字符串。*/
write (socket_fd, text, length);
}
int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
const char* const message = argv[2];
int socket_fd;
struct sockaddr_un name;
/* 创建套接字。*/
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
/* 将服务器地址写入套接字地址结构中。*/
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, socket_name);
/* 连接套接字。*/
connect (socket_fd, &name, SUN_LEN (&name));
/* 将由命令行指定的文本信息写入套接字。*/
write_text (socket_fd, message);
close (socket_fd);
return 0;
}
在客户端发送文本信息之前,客户端先通过发送整型变量length 的方式将消息的长度
通知服务端。类似的,服务端在读取消息之前先从套接字读取一个整型变量以获取消息的长
度。这提供给服务器一个在接收信息之前分配合适大小的缓冲区保存信息的方法。
要尝试这个例子,应在一个窗口中运行服务端程序。指定一个套接字文件的路径——例
如 /tmp/socket 作为参数:
% ./socket-server /tmp/socket
在另一个窗口中指明同一个套接字和消息,并多次运行客户端程序。
% ./socket-client /tmp/socket “Hello, world.”
% ./socket-client /tmp/socket “This is a test.”
服务端将接收并输出这些消息。要关闭服务端程序,从客户端发送“quit”即可:
% ./socket-client /tmp/socket “quit”
这样服务端程序就会退出。
5.5.6 Internet 域套接字
UNIX 域套接字只能用于同主机上的两个进程之间通信。Internet 域套接字则可以用来
连接网络中不同主机上的进程。
用于在Internet范围连接不同进程的套接字属于 Internet命名空间,使用PF_INET表示。
最常用的协议是TCP/IP。Internet 协议(Internet Protocol ,IP )是一个低层次的协议,负责
包在Internet 中的传递,并在需要的时候负责分片和重组数据包。它只保证“尽量”地发送,
因此包可能在传输过程中丢失,或者前后顺序被打乱。参与传输的每台主机都由一个独一无
二的IP 数字标识。传输控制协议(Transmission Control Protocol ,TCP )架构于 IP 协议之
上,提供了可靠的面向连接的传输。它允许主机之间建立类似电话的连接且保证数据传输的
可靠性和有序性。
Internet 套接字的地址包含两个部分:主机和端口号。这些信息保存在sockaddr_in
结构中。将 sin_family 字段设置为AF_INET以表示这是一个Internet 命名空间地址。目
标主机的Internet 地址作为一个 32位整数保存在sin_addr 字段中。端口号用于区分同一台
主机上的不同套接字。因为不同主机可能将多字节的值按照不同的字节序存储,应将htons
将端口号转换为网络字节序。参看ip 的手册页以获取更多信息。
可以通过调用gethostbyname 函数将一个可读的主机名——包括标准的以点分割的
IP 地址(如 10.0.0.1 )或DNS 名(如 www.codesourcery.com )——转换为32位IP 数
字。这个函数返回一个指向struct hosten t 结构的指针;其中的h_addr 字段包含了主
机的IP 数字。参考列表5.12 中的示例程序。
列表5.12 展示了 Internet 域套接字的使用。这个程序会获取由命令行指定的网页服务器
的首页。
代码列表 5.12 (socket-inet.c)从 WWW 服务器读取信息
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
/* 从服务器套接字中读取主页内容。返回成功的标记。*/
void get_home_page (int socket_fd)
{
char buffer[10000];
ssize_t number_characters_read;
/* 发送 HTTP GET 请求获取主页内容。*/
sprintf (buffer, “GET /\n”);
write (socket_fd, buffer, strlen (buffer));
/* 从套接字中读取信息。调用 read 一次可能不会返回全部信息,所以我们必须不
断尝试读取直到真正结束。*/
while (1) {
number_characters_read = read (socket_fd, buffer, 10000);
if (number_characters_read == 0)
return;
/* 将数据输出到标准输出流。*/
fwrite (buffer, sizeof (char), number_characters_read, stdout);
}
}
int main (int argc, char* const argv[])
{
int socket_fd;
struct sockaddr_in name;
struct hostent* hostinfo;
/* 创建套接字。*/
socket_fd = socket (PF_INET, SOCK_STREAM, 0);
/* 将服务器地址保存在套接字地址中。*/
name.sin_family = AF_INET;
/* 将包含主机名的字符串转换为数字。*/
hostinfo = gethostbyname (argv[1]);
if (hostinfo == NULL)
return 1;
else
name.sin_addr = *((struct in_addr *) hostinfo->h_addr);
/* 网页服务器使用 80 端口。*/
name.sin_port = htons (80);
/* 连接到网页服务器。*/
if (connect (socket_fd, &name, sizeof (struct sockaddr_in)) == -1)
{
perror (“connect”);
return 1;
}
/* 读取主页内容。*/
get_home_page (socket_fd);
return 0;
}
这个程序从命令行读取服务器的主机名(不是URL——也就是说,地址中不包括
“http://”部分)。它通过调用gethostbyname 将主机名转换为IP 地址,然后与主机的
80端口建立一个流式(TCP 协议的)套接字。网页服务器通过超文本传输协议(HyperText
Transport Protocol ,HTTP),因此程序发送 HTTP GET命令给服务器,而服务器传回主页
内容作为响应。
例如,可以这样运行程序从www.codesourcery.com 获取主页:
% ./socket-inet www.codesourcery.com
<html>
<meta http-equiv="Content-Type" content="text/html;
charset=iso-8859-1">
..
标准端口号
根据习惯,网页服务器在 80 端口监听客户端连接。多数 Internet 网络服务都被分配
了标准端口号。例如,使用 SSL 的安全网页服务器的在443 端口监听连接,而邮件服务
器(利用SMTP 协议通信)使用端口25 。
在GNU/Linux 系统中,协议——服务名关系列表被保存在了/etc/services 。
该文件的第一栏是协议或服务名,第二栏列举了对应的端口号和连接类型:tcp 代表
了面向连接的协议,而udp 代表数据报类型的。
如果你用Internet 域套接字实现了一个自己的协议,应使用高于 1024 的端口号进
行监听。
5.5.7 套接字对
如前所示,pipe 函数创建了两个文件描述符,分别代表管道的两端。管道有所限制因
为文件描述符只能被相关进程使用且经由管道进行的通信是单向的。而socketpair 函数
为一台主机上的一对相连接的的套接字创建两个文件描述符。这对文件描述符允许相关进程
之间进行双向通信。
它的前三个参数与socket 系统调用相同:分别指明了域、通信类型(译者著:原文为
connection style 连接类型,与前文不符,特此修改)和协议。最后一个参数是一个包
含两个元素的整型数组,用于保存创建的两个套接字的文件描述符,与pipe 的参数类似。
当调用socketpair 的时候,必须指定PF_LOCAL作为域。