Unix Sockets 简明教程
What is a Socket?
套接字允许同一台或不同机器上的两个进程之间通信。说得更精确一些,套接字是一种使用标准 Unix 文件描述符与其他计算机通信的方式。在 Unix 中,每个 I/O 操作都是通过写入或读取文件描述符来完成的。文件描述符只是一个与打开的文件相关联的整数,它可以是网络连接、文本文件、终端或其他东西。
对于程序员来说,套接字的外观和行为与低级文件描述符非常相似。这是因为 read() 和 write() 等命令与套接字一起工作的原理与它们与文件和管道一起工作的原理相同。
套接字最早出现于 2.1BSD 中,随后在 4.2BSD 中被改进为当前形式。现在,大多数当前 UNIX 系统发行版都可以使用套接字功能。
Where is Socket Used?
Unix 套接字用于客户端-服务器应用程序框架。服务器是一个根据客户端请求执行某些功能的进程。大多数应用程序级协议(如 FTP、SMTP 和 POP3)都使用套接字在客户端和服务器之间建立连接,然后交换数据。
Socket Types
用户可以使用四种类型的套接字。前两种最常用,后两种很少用。
通常认为进程只能在同类型套接字之间进行通信,但是没有规定阻止不同类型套接字之间的通信。
-
Stream Sockets − 在网络环境中保证交付。如果您通过流套接字发送三个项目“A、B、C”,它们将按相同顺序到达 − “A、B、C”。这些套接字使用 TCP(传输控制协议)进行数据传输。如果无法交付,发件人将收到一个错误指示器。数据记录没有任何边界。
-
Datagram Sockets − 在网络环境中不保证交付。它们是无连接的,因为您不需要像流套接字那样保持开放连接 − 只需构建一个包含目标信息的数据包然后发送出去即可。它们使用 UDP(用户数据报协议)。
-
Raw Sockets − 这些套接字为用户提供了访问基础通信协议的功能,这些协议支持套接字抽象。这些套接字通常面向数据报,尽管它们的具体特征取决于协议提供的接口。原始套接字并非面向普通用户;它们主要是为那些有兴趣开发新的通信协议或想要访问现有协议的某些更隐晦功能的人提供的。
-
Sequenced Packet Sockets − 它们类似于流套接字,但保留了记录边界。此接口仅作为网络系统 (NS) 套接字抽象的一部分提供,并且在大多数重要的 NS 应用程序中非常重要。顺序数据包套接字允许用户操作数据包或一组数据包上的顺序数据包协议 (SPP) 或互联网数据报协议 (IDP) 标头,方法是随要发送的任何数据编写一个原型标头,或者指定一个默认标头以在所有传出数据中使用,并且允许用户接收传入数据包的标头。
Unix Socket - Network Addresses
在我们继续进行实际操作之前,让我们简要讨论一下网络地址 − IP 地址。
IP 主机地址,或更通俗的说 IP 地址,用于识别连接到互联网的主机。IP 代表互联网协议,是指互联网的整体网络架构中的互联网层。
IP 地址是一个 32 位量,被解释为四个 8 位数或八位字节。每个 IP 地址唯一标识参与用户网络、网络上的主机以及用户网络的类。
IP 地址通常以点分十进制表示法 N1.N2.N3.N4 编写,其中每个 Ni 都是一个介于 0 到 255 的十进制数字(00 到 FF 十六进制)。
Address Classes
IP 地址由互联网号码分配局 (IANA) 管理和创建。有五种不同的地址类。您可以通过检查 IP 地址的前四个位来确定 IP 地址属于哪个类。
-
Class A 地址以 0xxx ,或 1 to 126 十进制开头。
-
Class B 地址以 10xx ,或 128 to 191 十进制开头。
-
Class C 地址以 110x ,或 192 to 223 十进制开头。
-
Class D 地址以 1110 ,或 224 to 239 十进制开头。
-
Class E 地址以 1111 或 240 to 254 十进制开头。
以 01111111 或 127 十进制开头的地址保留用于环回和本地计算机的内部测试 [您可以测试这一点:您应该始终能够 ping 127.0.0.1 ,它指向您自己];D 类地址保留用于多播;E 类地址保留供将来使用。不应将其用于主机地址。
Unix Socket - Network Host Names
就数字而言,主机名很难记住,因此它们被称为普通名称,例如 Takshila 或 Nalanda。我们编写软件应用程序来查找与给定名称对应的点分 IP 地址。
根据给定的字母数字主机名找出点分 IP 地址的过程称为 hostname resolution 。
由驻留在高容量系统上的特殊软件执行主机名解析。这些系统称为域名系统 (DNS),它保留 IP 地址和相应的普通名称的映射。
The /etc/hosts File
主机名与 IP 地址之间的对应关系保存在一个名为 hosts 的文件中。在大多数系统上,此文件位于 /etc 目录中。
此文件中的条目看起来如下——
# This represents a comments in /etc/hosts file.
127.0.0.1 localhost
192.217.44.207 nalanda metro
153.110.31.18 netserve
153.110.31.19 mainserver centeral
153.110.31.20 samsonite
64.202.167.10 ns3.secureserver.net
64.202.167.97 ns4.secureserver.net
66.249.89.104 www.google.com
68.178.157.132 services.amrood.com
请注意,多个名称可能与给定的 IP 地址关联。转换 IP 地址到主机名和反之亦然时,将使用此文件。
您无法访问此文件进行编辑,因此,如果您想将任何主机名与 IP 地址一起放入,那么您需要具有 root 权限。
Unix Socket - Client Server Model
大多数网络应用程序都使用客户端-服务器架构,它表示两个进程或两个应用程序彼此通信以交换一些信息。两个进程之一充当客户端进程,另一个进程充当服务器。
Client Process
这是该进程,它通常会请求信息。收到响应后,该进程可能会终止或可能会执行一些其他处理。
Example ,互联网浏览器作为客户端应用程序运行,它向 Web 服务器发送请求以获取一个 HTML 网页。
Server Process
这是从客户端获取请求的进程。从客户端获取请求后,此进程将执行必需的处理,收集请求的信息并将其发送给请求方客户端。完成后,它将准备好为另一个客户端提供服务。服务器进程始终处于警报状态并随时准备服务传入请求。
Example - Web 服务器不断等待互联网浏览器的请求,并且一旦它收到来自浏览器的任何请求,它就会挑选请求的 HTML 页面并将其发回该浏览器。
注意,客户端需要知道服务器的地址,但是服务器不需要知道客户端的地址甚至存在,直到建立连接后。建立连接后,双方都可以发送和接收信息。
2-tier and 3-tier architectures
有两种类型的客户端-服务器架构 −
-
2-tier architecture − 在此架构中,客户端直接与服务器交互。这种类型的架构可能存在一些安全漏洞和性能问题。Internet Explorer 和 Web 服务器使用两层架构。在此,使用安全套接字层 (SSL) 来解决安全问题。
-
3-tier architectures − 在此架构中,另一个软件位于客户端和服务器之间。此中间软件称为“中间件”。使用中间件来执行所有安全检查并在负载量大的情况下进行负载平衡。中间件从客户端接收所有请求,并执行必需的身份验证后,将请求传递给服务器。然后,服务器执行必需的处理并将响应发送回中间件,最终中间件将此响应传递回客户端。如果您想实现 3 层架构,那么您可以在 Web 服务器和 Web 浏览器之间放置任何中间件(如 Web Logic 或 WebSphere 软件)。
Types of Server
您可以有两种类型的服务器 −
-
Iterative Server − 这是服务器的最简单形式,其中一个服务器进程服务一个客户端,并且在完成第一个请求后,接收来自另一个客户端的请求。同时,另一个客户端保持等待状态。
-
Concurrent Servers − 这种类型的服务器运行多个并发进程以一次处理多个请求,因为一个进程可能需要更长时间,并且另一个客户端不能等待这么长时间。在 Unix 下编写并发服务器最简单的方法是为每个客户端派生一个子进程以单独处理。
How to Make Client
用于建立连接的系统调用对于客户端和服务器来说是不同的,但两者都涉及套接字的基本结构。这两个进程都建立自己的套接字。
在客户端建立套接字涉及以下步骤 −
-
使用 socket() 系统调用创建一个套接字。
-
使用 connect() 系统调用将套接字连接到服务器的地址。
-
发送和接收数据。有许多方法可以做到这一点,但最简单的方法是使用 read() 和 write() 系统调用。
Unix Socket - Structures
Unix 套接字编程中使用各种结构来保存有关地址和端口等信息的的信息。大多数套接字函数需要一个指向套接字地址结构的指针作为参数。本章定义的结构与 Internet 协议族相关。
sockaddr
第一个结构是 sockaddr,它保存套接字信息——
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
这是一个通用的套接字地址结构,它将在大多数套接字函数调用中传递。下表提供了成员字段的说明——
Attribute |
Values |
Description |
sa_family |
AF_INET AF_UNIX AF_NS AF_IMPLINK |
它表示地址族。在大多数基于 Internet 的应用程序中,我们使用 AF_INET。 |
sa_data |
Protocol-specific Address |
根据地址的类型解释协议特定地址的 14 个字节的内容。对于 Internet 族,我们将使用端口号 IP 地址,它由下面定义的 sockaddr_in 结构表示。 |
sockaddr in
帮助你引用套接字元素的第二个结构如下所示 −
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
下面是成员字段的说明 −
Attribute |
Values |
Description |
sa_family |
AF_INET AF_UNIX AF_NS AF_IMPLINK |
它表示地址族。在大多数基于 Internet 的应用程序中,我们使用 AF_INET。 |
sin_port |
Service Port |
以网络字节顺序排列的 16 位端口号。 |
sin_addr |
IP Address |
以网络字节顺序排列的 32 位 IP 地址。 |
sin_zero |
Not Used |
你只需将该值设置为 NULL,因为未使用。 |
in addr
此结构仅用作以上结构中的结构字段,并保存 32 位网络 ID/主机 ID。
struct in_addr {
unsigned long s_addr;
};
下面是成员字段的说明 −
Attribute |
Values |
Description |
s_addr |
service port |
以网络字节顺序排列的 32 位 IP 地址。 |
hostent
此结构用于保存与主机相关的的信息。
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list
#define h_addr h_addr_list[0]
};
下面是成员字段的说明 −
Attribute |
Values |
Description |
h_name |
ti.com etc. |
这是主机的官方名称。例如,tutorialspoint.com、google.com 等。 |
h_aliases |
TI |
保存主机名称别名列表。 |
h_addrtype |
AF_INET |
包含地址族,在基于因特网的应用程序中,它将始终为 AF_INET。 |
h_length |
4 |
保存 IP 地址的长度,因特网地址为 4。 |
h_addr_list |
in_addr |
对于因特网地址,h_addr_list[0]、h_addr_list[1] 等指针数组指向 in_addr 结构。 |
NOTE − 为保持向后兼容性,h_addr 定义为 h_addr_list[0]。
servent
此特定结构用于保存与服务和相关端口有关的信息。
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
};
下面是成员字段的说明 −
Attribute |
Values |
Description |
s_name |
http |
这是服务的官方名称。例如,SMTP、FTP POP3 等。 |
s_aliases |
ALIAS |
保存服务别名列表。大多数情况下此内容将设置为 NULL。 |
s_port |
80 |
将有相关端口号。例如,对于 HTTP,此端口号为 80。 |
s_proto |
TCP UDP |
它设置为使用的协议。互联网服务通过 TCP 或 UDP 提供。 |
Unix Socket - Ports and Services
当客户端进程想连接服务器时,客户端必须有一种方法来识别它想要连接的服务器。如果客户端知道驻留服务器的主机的 32 位互联网地址,它就可以联系该主机。但是客户端如何识别该主机上正在运行的特定服务器进程?
为了解决在主机上识别特定服务器进程的问题,TCP 和 UDP 都定义了一组众所周知端口。
就我们的目的而言,端口将被定义为 1024 到 65535 之间的整数。这是因为小于 1024 的所有端口号都被认为是众所周知的——例如,Telnet 使用端口 23,http 使用 80,ftp 使用 21,依此类推。
网络服务的端口分配可以在文件 /etc/services 中找到。如果您要编写自己的服务器,则必须小心为服务器分配一个端口。您应该确保此端口未分配给任何其他服务器。
通常做法是分配大于 5000 的任何端口号。但有许多组织编写的服务器具有大于 5000 的端口号。例如,Yahoo Messenger 在 5050 上运行,SIP 服务器在 5060 上运行,等等。
Example Ports and Services
以下是一份服务和关联端口的小列表。您可以在 IANA - TCP/IP Port Assignments 找到最更新的互联网端口和关联服务列表。
Service |
Port Number |
Service Description |
echo |
7 |
UDP/TCP 发回它接收的内容。 |
discard |
9 |
UDP/TCP throws away input. |
daytime |
13 |
UDP/TCP returns ASCII time. |
chargen |
19 |
UDP/TCP returns characters. |
ftp |
21 |
TCP file transfer. |
telnet |
23 |
TCP remote login. |
smtp |
25 |
TCP email. |
daytime |
37 |
UDP/TCP returns binary time. |
tftp |
69 |
UDP trivial file transfer. |
finger |
79 |
TCP info on users. |
http |
80 |
TCP World Wide Web. |
login |
513 |
TCP remote login. |
who |
513 |
UDP 在用户上展示不同的信息。 |
Xserver |
6000 |
TCP X 视窗(注意:>1023)。 |
Port and Service Functions
Unix 提供下列函数用于从 /etc/services 文件中获取服务名称。
-
struct servent *getservbyname(char *name, char *proto) − 该调用获取服务名称和协议名称,并为此服务返回相应端口号。
-
struct servent *getservbyport(int port, char *proto) − 该调用获取端口号和协议名称,并返回相应服务名称。
每个函数的返回值都是一个指向具有下列形式的结构的指针 −
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
};
下面是成员字段的说明 −
Attribute |
Values |
Description |
s_name |
http |
这是服务的正式名称。例如,SMTP、FTP、POP3 等。 |
s_aliases |
ALIAS |
它保存服务别名的列表。大多数情况下,它将设置为 NULL。 |
s_port |
80 |
它将具有关联的端口号。例如,对于 HTTP,它将为 80。 |
s_proto |
TCP UDP |
它设置为使用的协议。互联网服务通过 TCP 或 UDP 提供。 |
Unix Socket - Network Byte Orders
不幸的是,并非所有计算机都以相同的顺序存储构成多字节值的那个字节。考虑一个由 2 个字节组成的 16 位互联网。有两种方法来存储此值。
-
Little Endian − 在此方案中,低位字节存储在起始地址 (A) 上,而高位字节存储在下一个地址 (A + 1) 上。
-
Big Endian − 在此方案中,高位字节存储在起始地址 (A) 上,而低位字节存储在下一个地址 (A + 1) 上。
为了允许具有不同字节顺序约定的机器互相通信,因特网协议为通过网络传输的数据指定了一个规范的字节顺序约定。这被称为网络字节顺序。
在建立 Internet 套接字连接时,您必须确保 sockaddr_in 结构的 sin_port 和 sin_addr 成员中的数据以网络字节顺序表示。
Byte Ordering Functions
用于在主机内部表示和网络字节顺序之间转换数据的例程如下 -
Function |
Description |
htons() |
Host to Network Short |
htonl() |
Host to Network Long |
ntohl() |
Network to Host Long |
ntohs() |
Network to Host Short |
下面列出了一些有关这些函数的更多详细信息 −
-
unsigned short htons(unsigned short hostshort) − 此函数将 16 位(2 字节)量从主机字节顺序转换为网络字节顺序。
-
unsigned long htonl(unsigned long hostlong) − 此函数将 32 位(4 字节)量从主机字节顺序转换为网络字节顺序。
-
unsigned short ntohs(unsigned short netshort) − 此函数将 16 位(2 字节)量从网络字节顺序转换为主机字节顺序。
-
unsigned long ntohl(unsigned long netlong) − 此函数将 32 位量从网络字节顺序转换为主机字节顺序。
这些函数是宏,并导致将转换源代码插入到调用程序中。在小端机器上,代码会将值更改为网络字节顺序。在大型机器上,不插入任何代码,因为不需要这些代码;这些函数被定义为 null。
Program to Determine Host Byte Order
将以下代码保存在一个名为 byteorder.c 的文件中,然后编译它并在机器上运行。
在此示例中,我们将两个字节值 0x0102 存储在 short integer 中,然后查看两个连续的字节 c[0](地址 A)和 c[1](地址 A + 1)以确定字节顺序。
#include <stdio.h>
int main(int argc, char **argv) {
union {
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if (sizeof(short) == 2) {
if (un.c[0] == 1 && un.c[1] == 2)
printf("big-endian\n");
else if (un.c[0] == 2 && un.c[1] == 1)
printf("little-endian\n");
else
printf("unknown\n");
}
else {
printf("sizeof(short) = %d\n", sizeof(short));
}
exit(0);
}
此程序在奔腾机器上生成的输出如下 -
$> gcc byteorder.c
$> ./a.out
little-endian
$>
Unix Socket - IP Address Functions
Unix 提供了各种函数调用,可以帮助你处理 IP 地址。这些函数在 ASCII 字符串(人类更喜欢使用)和网络字节序二进制值(存储在套接字地址结构中的值)之间转换互联网地址。
以下三个函数调用用于 IPv4 寻址:
-
int inet_aton(const char *strptr, struct in_addr *addrptr)
-
in_addr_t inet_addr(const char *strptr)
-
char *inet_ntoa(struct in_addr inaddr)
int inet_aton(const char *strptr, struct in_addr *addrptr)
此函数调用将互联网标准点分十进制表示法中的指定字符串转换为网络地址,并将地址存储在所提供的结构中。转换后的地址将采用网络字节序(从左到右排序字节)。如果字符串有效则返回 1,如果出错则返回 0。
以下是在使用示例:
#include <arpa/inet.h>
(...)
int retval;
struct in_addr addrptr
memset(&addrptr, '\0', sizeof(addrptr));
retval = inet_aton("68.178.157.132", &addrptr);
(...)
in_addr_t inet_addr(const char *strptr)
此函数调用将互联网标准点分十进制表示法中的指定字符串转换为适合用作互联网地址的整数值。转换后的地址将采用网络字节序(从左到右排序字节)。它返回一个 32 位二进制网络字节序 IPv4 地址,如果出错,则返回 INADDR_NONE。
以下是在使用示例:
#include <arpa/inet.h>
(...)
struct sockaddr_in dest;
memset(&dest, '\0', sizeof(dest));
dest.sin_addr.s_addr = inet_addr("68.178.157.132");
(...)
Unix Socket - Core Functions
本章介绍了编写完整的 TCP 客户端和服务器所需的核心套接字函数。
下图显示了完整的客户端和服务器交互 -
The socket Function
为了执行网络 I/O,进程必须做的第一件事是调用套接字函数,指定所需的通信协议类型和协议族等。
#include <sys/types.h>
#include <sys/socket.h>
int socket (int family, int type, int protocol);
此调用会返回一个套接字描述符,可以在后续的系统调用中使用,或在出错时返回 -1。
Parameters
family − 指定协议系列,是下面所示的常量之一 −
Family |
Description |
AF_INET |
IPv4 protocols |
AF_INET6 |
IPv6 protocols |
AF_LOCAL |
Unix domain protocols |
AF_ROUTE |
Routing Sockets |
AF_KEY |
Ket socket |
本章不涉及 IPv4 以外的其他协议。
type − 指定你想要哪种套接字。可以采用以下值之一 −
Type |
Description |
SOCK_STREAM |
Stream socket |
SOCK_DGRAM |
Datagram socket |
SOCK_SEQPACKET |
Sequenced packet socket |
SOCK_RAW |
Raw socket |
protocol − 应该将参数设置为下面给出的特定协议类型,或设置为 0 以选择系统针对给定的系列和类型的默认项 −
Protocol |
Description |
IPPROTO_TCP |
TCP transport protocol |
IPPROTO_UDP |
UDP transport protocol |
IPPROTO_SCTP |
SCTP transport protocol |
The connect Function
connect 函数由 TCP 客户端用于建立与 TCP 服务器的连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
如果成功连接到服务器,此调用会返回 0,否则在出错时会返回 -1。
The bind Function
bind 函数将本地协议地址分配给套接字。使用 Internet 协议时,该协议地址由一个 32 位 IPv4 地址或一个 128 位 IPv6 地址,以及一个 16 位 TCP 或 UDP 端口号相结合构成。该函数仅由 TCP 服务器调用。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr,int addrlen);
如果成功绑定到该地址,此调用会返回 0,否则在出错时会返回 -1。
Parameters
-
sockfd − 由 socket 函数返回的套接字描述符。
-
my_addr − 指向包含本地 IP 地址和端口的 struct sockaddr。
-
addrlen − 将其设置为 sizeof(struct sockaddr)。
你可以自动放入你的 IP 地址和端口
端口号的 0 值表示系统将选择一个随机端口,并且 IP 地址的 INADDR_ANY 值表示将自动分配服务器的 IP 地址。
server.sin_port = 0;
server.sin_addr.s_addr = INADDR_ANY;
NOTE − 低于 1024 的所有端口都是保留的。你可以设置 1024 以上和 65535 以下的端口,除非它们被其他程序使用。
The listen Function
listen 函数仅由 TCP 服务器调用,并且它执行两个操作 −
-
listen 函数将未连接的套接字转换为一个被动套接字,表示内核应该接受定向到此套接字的传入连接请求。
-
传递给此函数的第二个参数指定内核应该为该套接字排队的最大连接数。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd,int backlog);
此调用成功时返回 0,否则在错误时返回 -1。
The accept Function
accept 函数由 TCP 服务器调用,以从已完成连接队列最前端返回下一个已完成连接。调用的签名如下:
#include <sys/types.h>
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
此调用成功时返回非负描述符,否则在错误时返回 -1。假定返回的描述符为客户端套接字描述符,并且所有读写操作都将在此描述符上执行以与客户端进行通信。
The send Function
send 函数用于通过流套接字或已连接的数据报套接字发送数据。如果您想通过未连接的数据报套接字发送数据,则必须使用 sendto() 函数。
您可以使用 write() 系统调用来发送数据。其签名如下:
int send(int sockfd, const void *msg, int len, int flags);
此调用返回发送的字节数,否则在错误时返回 -1。
The recv Function
recv 函数用于通过流套接字或已连接的数据报套接字接收数据。如果您想通过未连接的数据报套接字接收数据,则必须使用 recvfrom()。
您可以使用 read() 系统调用来读取数据。此调用在帮助程序函数章节中进行了解释。
int recv(int sockfd, void *buf, int len, unsigned int flags);
此调用返回读入缓冲区的字节数,否则在错误时返回 -1。
The sendto Function
sendto 函数用于通过未连接的数据报套接字发送数据。其签名如下:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
此调用返回发送的字节数,否则在错误时返回 -1。
The recvfrom Function
recvfrom 函数用来从未连接的数据报套接字接收数据。
int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);
此调用返回读取到缓冲区中的字节数,否则在出错时返回 -1。
The shutdown Function
shutdown 函数用来优雅地关闭客户端和服务器之间的通信。与 close 函数相比,此函数提供更多控制。以下是 shutdown 的语法:
int shutdown(int sockfd, int how);
此调用成功时返回 0,否则在错误时返回 -1。
The select Function
select 函数指示指定的文件描述符中的哪一个已准备好进行读取、准备好进行写入,或有待处理的错误条件。
当一个应用程序调用 recv 或 recvfrom 时,它会被阻塞,直到针对该套接字到达数据。当输入数据流为空时,一个应用程序可以执行其他有用的处理。另一种情况是当一个应用程序从多个套接字接收数据时。
在一个输入队列中没有数据的套接字上调用 recv 或 recvfrom 会阻止立即从其他套接字接收数据。select 函数调用通过允许程序轮询所有套接字句柄来解决此问题,以查看它们是否可用于非阻塞读取和写入操作。
以下是 select 的语法:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
此调用成功时返回 0,否则在错误时返回 -1。
Parameters
-
nfds − 它指定要测试的文件描述符范围。select() 函数测试范围为 0 到 nfds-1 的文件描述符
-
readfds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否已准备好进行读取,并且在输出时,它指示哪些文件描述符已准备好进行读取。它可以为 NULL,以指示一个空集。
-
writefds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否已准备好进行写入,并且在输出时,它指示哪些文件描述符已准备好进行写入。它可以为 NULL,以指示一个空集。
-
exceptfds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否有待处理的错误条件,并且在输出时,它指示哪些文件描述符有待处理的错误条件。它可以为 NULL,以指示一个空集。
-
timeout − 它指向一个 timeval struct,该 struct 指定 select 调用应该轮询描述符多长时间,以获得一个可用的 I/O 操作。如果超时值为 0,则 select 将立即返回。如果 timeout 参数为 NULL,则 select 将阻塞,直至至少有一个文件/套接字句柄已准备好进行可用的 I/O 操作。否则,select 将在 timeout 中的时间量经过或至少有一个文件/套接字描述符已准备好进行 I/O 操作后返回。
来自 select 的返回值是文件描述符集中指定的文件句柄数,这些句柄已准备好进行 I/O。如果 timeout 字段指定的时间限制达到,select 返回 0。以下宏用于操作文件描述符集:
-
FD_CLR(fd, &fdset) − 清除文件描述符集 fdset 中文件描述符 fd 的位。
-
FD_ISSET(fd, &fdset) − 如果文件描述符集中由fdset 指向的文件描述符的位被设置,则返回一个非零值;否则返回 0。
-
FD_SET(fd, &fdset) − 在文件描述符集中 fdset 中设置文件描述符 fd 的位。
-
FD_ZERO(&fdset) − 初始化文件描述符集 fdset 以便所有文件描述符的位都为零。
如果 fd 参数小于 0 或大于或等于 FD_SETSIZE,则这些宏的行为是未定义的。
Example
fd_set fds;
struct timeval tv;
/* do socket initialization etc.
tv.tv_sec = 1;
tv.tv_usec = 500000;
/* tv now represents 1.5 seconds */
FD_ZERO(&fds);
/* adds sock to the file descriptor set */
FD_SET(sock, &fds);
/* wait 1.5 seconds for any data to be read from any single socket */
select(sock+1, &fds, NULL, NULL, &tv);
if (FD_ISSET(sock, &fds)) {
recvfrom(s, buffer, buffer_len, 0, &sa, &sa_len);
/* do something */
}
else {
/* do something else */
}
Unix Socket - Helper Functions
本章描述了所有辅助函数,这些函数在执行套接字编程时使用。其他辅助函数在章节中描述 - Ports and Services 和网络 Byte Orders 。
The write Function
write 函数尝试将 buf 指向的缓冲区中的 nbyte 字节写入与打开的文件描述符 fildes 关联的文件。
您还可以使用 send() 函数向另一个进程发送数据。
#include <unistd.h>
int write(int fildes, const void *buf, int nbyte);
成功完成后,write() 返回实际写入与 fildes 关联的文件的字节数。此数字永远不会大于 nbyte。否则,返回 -1。
The read Function
read 函数尝试从与缓冲区 fildes 关联的文件读取 nbyte 字节,并将其读入 buf 指向的缓冲区。
您还可以使用 recv() 函数读取另一个进程的数据。
#include <unistd.h>
int read(int fildes, const void *buf, int nbyte);
成功完成后,write() 返回实际写入与 fildes 关联的文件的字节数。此数字永远不会大于 nbyte。否则,返回 -1。
The fork Function
fork 函数创建一个新进程。称为子进程的新进程将是调用进程(父进程)的确切副本。子进程从父进程继承许多属性。
#include <sys/types.h>
#include <unistd.h>
int fork(void);
成功完成后,fork() 将 0 返回给子进程,并将子进程的进程 ID 返回给父进程。否则,返回 -1 给父进程,不会创建子进程,并设置 errno 指示错误。
The bzero Function
bzero 函数在字符串 s 中放置 nbyte 个空字节。此函数用于使用空值设置所有套接字结构。
void bzero(void *s, int nbyte);
此函数不会返回任何内容。
The bcmp Function
bcmp 函数将字节字符串 s1 与字节字符串 s2 进行比较。假设这两个字符串都是 nbyte 字节长。
int bcmp(const void *s1, const void *s2, int nbyte);
如果这两个字符串相同,则此函数返回 0,否则返回 1。如果 nbyte 为 0,bcmp() 函数始终返回 0。
Unix Socket - Server Examples
若要使进程成为 TCP 服务器,您需要遵循以下步骤 −
-
使用 socket() 系统调用创建一个套接字。
-
使用 bind() 系统调用将套接字绑定到一个地址。对于 Internet 上的服务器套接字来说,一个地址由主机上的端口号组成。
-
用 listen() 系统调用侦听连接。
-
使用 accept() 系统调用接受连接。该调用通常会阻塞,直到客户端连接到服务器。
-
使用 read() 和 write() 系统调用发送和接收数据。
现在我们将这些步骤放在源代码的形式中。将此代码添加到文件 server.c 中,并使用 gcc 编译器进行编译。
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
int main( int argc, char *argv[] ) {
int sockfd, newsockfd, portno, clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
int n;
/* First call to socket() function */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
/* Initialize socket structure */
bzero((char *) &serv_addr, sizeof(serv_addr));
portno = 5001;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
/* Now bind the host address using bind() call.*/
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR on binding");
exit(1);
}
/* Now start listening for the clients, here process will
* go in sleep mode and will wait for the incoming connection
*/
listen(sockfd,5);
clilen = sizeof(cli_addr);
/* Accept actual connection from the client */
newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
if (newsockfd < 0) {
perror("ERROR on accept");
exit(1);
}
/* If connection is established then start communicating */
bzero(buffer,256);
n = read( newsockfd,buffer,255 );
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("Here is the message: %s\n",buffer);
/* Write a response to the client */
n = write(newsockfd,"I got your message",18);
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
return 0;
}
Handle Multiple Connections
为了允许服务器处理多个同时连接,我们在上述代码中进行了以下更改 -
-
将 accept 语句和以下代码放在一个无限循环中。
-
在建立连接之后,调用 fork() 创建一个新进程。
-
子进程将关闭 sockfd 并调用 doprocessing 函数,将新的套接字文件描述符作为参数传递。当 doprocessing() 返回表示两个进程已完成其对话后,此进程便退出。
-
父进程关闭 newsockfd。由于所有这些代码都在一个无限循环中,它将返回到 accept 语句等待下一个连接。
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
void doprocessing (int sock);
int main( int argc, char *argv[] ) {
int sockfd, newsockfd, portno, clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
int n, pid;
/* First call to socket() function */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
/* Initialize socket structure */
bzero((char *) &serv_addr, sizeof(serv_addr));
portno = 5001;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
/* Now bind the host address using bind() call.*/
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR on binding");
exit(1);
}
/* Now start listening for the clients, here
* process will go in sleep mode and will wait
* for the incoming connection
*/
listen(sockfd,5);
clilen = sizeof(cli_addr);
while (1) {
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) {
perror("ERROR on accept");
exit(1);
}
/* Create child process */
pid = fork();
if (pid < 0) {
perror("ERROR on fork");
exit(1);
}
if (pid == 0) {
/* This is the client process */
close(sockfd);
doprocessing(newsockfd);
exit(0);
}
else {
close(newsockfd);
}
} /* end of while */
}
以下代码片段展示了 doprocessing 函数的简单实现。
void doprocessing (int sock) {
int n;
char buffer[256];
bzero(buffer,256);
n = read(sock,buffer,255);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("Here is the message: %s\n",buffer);
n = write(sock,"I got your message",18);
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
}
Unix Socket - Client Examples
为了使进程成为 TCP 客户端,您需要按照以下步骤操作:
-
使用 socket() 系统调用创建一个套接字。
-
使用 connect() 系统调用将套接字连接到服务器的地址。
-
发送和接收数据。有很多方法可以做到这一点,但是最简单的方法是使用 read() 和 write() 系统调用。
现在我们将这些步骤放在源代码的形式中。将此代码添加到文件 client.c 中,并使用 gcc 编译器进行编译。
运行此程序并传递服务器的主机名和端口号,以连接到服务器,您必须已在另一个 Unix 窗口中运行服务器。
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
char buffer[256];
if (argc < 3) {
fprintf(stderr,"usage %s hostname port\n", argv[0]);
exit(0);
}
portno = atoi(argv[2]);
/* Create a socket point */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
server = gethostbyname(argv[1]);
if (server == NULL) {
fprintf(stderr,"ERROR, no such host\n");
exit(0);
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
serv_addr.sin_port = htons(portno);
/* Now connect to the server */
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
exit(1);
}
/* Now ask for a message from the user, this message
* will be read by server
*/
printf("Please enter the message: ");
bzero(buffer,256);
fgets(buffer,255,stdin);
/* Send message to the server */
n = write(sockfd, buffer, strlen(buffer));
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
/* Now read server response */
bzero(buffer,256);
n = read(sockfd, buffer, 255);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("%s\n",buffer);
return 0;
}
Unix Socket - Summary
以下是与套接字编程相关的所有函数的列表。
Port and Service Functions
Unix 提供下列函数用于从 /etc/services 文件中获取服务名称。
-
struct servent *getservbyname(char *name, char *proto) - 此调用使用服务名称和协议名称,并返回该服务的对应端口号。
-
struct servent *getservbyport(int port, char *proto) - 此调用使用端口号和协议名称,并返回对应服务名称。
Byte Ordering Functions
-
unsigned short htons (unsigned short hostshort) - 此函数将 16 位(2 字节)量从主机字节顺序转换为网络字节顺序。
-
unsigned long htonl (unsigned long hostlong) - 此函数将 32 位(4 字节)数量从主机字节顺序转换为网络字节顺序。
-
unsigned short ntohs (unsigned short netshort) - 此函数将 16 位(2 字节)数量从网络字节顺序转换为主机字节顺序。
-
unsigned long ntohl (unsigned long netlong) - 此函数将 32 位数量从网络字节顺序转换为主机字节顺序。
IP Address Functions
-
int inet_aton (const char *strptr, struct in_addr *addrptr) - 此函数调用将互联网标准点符号表示形式中的指定字符串转换为网络地址,并将地址存储在指定结构中。转换后的地址将采用网络字节顺序(字节从左到右排序)。如果字符串有效,则它返回 1,如果出错,则返回 0。
-
in_addr_t inet_addr (const char *strptr) - 此函数调用将互联网标准点符号表示形式中的指定字符串转换为适合用作互联网地址的整数值。转换后的地址将采用网络字节顺序(字节从左到右排序)。它返回一个 32 位二进制网络字节顺序 IPv4 地址,如果出错,则返回 INADDR_NONE。
-
char *inet_ntoa (struct in_addr inaddr) - 此函数调用将指定的互联网主机地址转换为互联网标准点符号表示形式中的字符串。
Socket Core Functions
-
int socket (int family, int type, int protocol) - 此调用返回一个套接字描述符,你可以在稍后的系统调用中使用它,或者在出错时它会给你 -1。
-
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen) - connect 函数由 TCP 客户端用于建立与 TCP 服务器的连接。如果此调用成功连接到服务器,则它返回 0,否则它返回 -1。
-
int bind(int sockfd, struct sockaddr *my_addr,int addrlen) - bind 函数向套接字分配本地协议地址。如果此调用成功绑定到地址,则它返回 0,否则它返回 -1。
-
int listen(int sockfd, int backlog) - listen 函数仅由 TCP 服务器调用,以侦听客户端请求。此调用成功时返回 0,否则它返回 -1。
-
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen) - accept 函数由 TCP 服务器调用,以接受客户端请求并建立实际连接。此调用成功时返回一个非负描述符,否则它返回 -1。
-
int send(int sockfd, const void *msg, int len, int flags) - send 函数用于通过流套接字或已连接的数据报套接字发送数据。此调用返回已发送出的字节数,否则它返回 -1。
-
int recv (int sockfd, void *buf, int len, unsigned int flags) - recv 函数用于通过流套接字或已连接的数据报套接字接收数据。此调用返回已读入缓冲区的字节数,否则它返回 -1。
-
int sendto (int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen) - sendto 函数用于通过未连接的数据报套接字发送数据。此调用返回已发送出的字节数,否则它返回 -1。
-
int recvfrom (int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen) - recvfrom 函数用于从未连接的数据报套接字接收数据。此调用返回已读入缓冲区的字节数,否则它返回 -1。
-
int close (int sockfd) - close 函数用于关闭客户端和服务器之间的通信。此调用成功时返回 0,否则它返回 -1。
-
int shutdown (int sockfd, int how) - shutdown 函数用于优雅地关闭客户端和服务器之间的通信。与 close 函数相比,此函数提供了更多控制。此调用成功时返回 0,否则返回 -1。
-
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout) - 此函数用于读写多个套接字。
Socket Helper Functions
-
int write (int fildes, const void *buf, int nbyte) - write 函数尝试将 buf 指向的缓冲区中的 nbyte 字节写入与打开的文件描述符 fildes 关联的文件。如果成功完成,write() 将返回实际写入与 fildes 关联的文件的字节数。此数量永远不会大于 nbyte。否则,将返回 -1。
-
int read (int fildes, const void *buf, int nbyte) − read 函数尝试从与开放的文件描述符关联的文件 fildes,读取 nbyte 字节到 buf 指向的缓冲器内。在成功完成时,write() 返回实际写入到与 fildes 关联的文件内的字节数。此数字永远不会大于 nbyte。否则,将返回 -1。
-
int fork (void) − fork 函数创建一个新进程。新进程称为子进程,将成为调用进程(父进程)的确切副本。
-
void bzero (void *s, int nbyte) − bzero 函数放置 nbyte 空值到字符串 s 中。此函数将用来设置带空值的全部套接字结构。
-
int bcmp (const void *s1, const void *s2, int nbyte) − bcmp 函数比较字节字符串 s1 和字节字符串 s2。认为这两个字符串都是 nbyte 字节长。
-
void bcopy (const void *s1, void *s2, int nbyte) − bcopy 函数从字符串 s1 复制 nbyte 字节到字符串 s2 中。重叠字符串正确地被处理。
-
void *memset(void *s, int c, int nbyte) − memset 函数也被用来设置结构变量比如 bzero。