- Unix Socket 教程
- Unix Socket - 首页
- Unix Socket - 什么是 Socket?
- Unix Socket - 网络地址
- Unix Socket - 网络主机名
- Unix Socket - 客户端-服务器模型
- Unix Socket - 结构
- Unix Socket - 端口和服务
- Unix Socket - 网络字节序
- Unix Socket - IP 地址函数
- Unix Socket - 核心函数
- Unix Socket - 辅助函数
- Unix Socket - 服务器示例
- Unix Socket - 客户端示例
- Unix Socket - 总结
- Unix Socket 有用资源
- Unix Socket - 快速指南
- Unix Socket - 有用资源
- Unix Socket - 讨论
Unix Socket - 快速指南
什么是 Socket?
Sockets 允许在同一台或不同机器上的两个不同进程之间进行通信。更准确地说,它是一种使用标准 Unix 文件描述符与其他计算机通信的方式。在 Unix 中,每个 I/O 操作都是通过写入或读取文件描述符来完成的。文件描述符只是一个与打开的文件关联的整数,它可以是网络连接、文本文件、终端或其他东西。
对于程序员来说,socket 看起来和表现得都像一个低级文件描述符。这是因为像 read() 和 write() 这样的命令以与处理文件和管道相同的方式处理 socket。
Sockets 最初是在 2.1BSD 中引入的,随后在 4.2BSD 中被改进为其当前形式。现在大多数当前的 UNIX 系统版本都提供了 sockets 功能。
Socket 在哪里使用?
Unix Socket 用于客户端-服务器应用程序框架。服务器是一个进程,它根据客户端的请求执行某些功能。大多数应用程序级协议(如 FTP、SMTP 和 POP3)都使用 sockets 在客户端和服务器之间建立连接,然后交换数据。
Socket 类型
用户可以使用四种类型的 socket。前两种最常用,后两种很少使用。
假定进程仅在相同类型的 socket 之间通信,但没有限制阻止不同类型的 socket 之间的通信。
流式 Socket - 网络环境中的交付是有保证的。如果您通过流式 socket 发送三个项目“A、B、C”,它们将按相同的顺序到达 - “A、B、C”。这些 socket 使用 TCP(传输控制协议)进行数据传输。如果无法交付,发送方会收到错误指示。数据记录没有任何边界。
数据报 Socket - 网络环境中的交付没有保证。它们是无连接的,因为您不需要像流式 socket 那样打开连接 - 您构建一个包含目标信息的包并将其发送出去。它们使用 UDP(用户数据报协议)。
原始 Socket - 这些 socket 为用户提供了对支持 socket 抽象的基础通信协议的访问。这些 socket 通常是面向数据报的,尽管它们的精确特性取决于协议提供的接口。原始 socket 不是为一般用户设计的;它们主要提供给那些有兴趣开发新通信协议或访问现有协议的一些更神秘功能的人。
顺序分组 Socket - 它们类似于流式 socket,但保留了记录边界。此接口仅作为网络系统 (NS) socket 抽象的一部分提供,并且在大多数严肃的 NS 应用程序中非常重要。顺序分组 socket 允许用户通过以下方式操作包或一组包上的顺序分组协议 (SPP) 或互联网数据报协议 (IDP) 标头:编写一个原型标头以及要发送的任何数据,或指定一个默认标头用于所有传出数据,并允许用户接收传入包上的标头。
接下来是什么?
接下来的几章旨在巩固您的基础,并在您能够使用socket编写服务器和客户端程序之前奠定基础。如果您想直接跳到如何编写客户端和服务器程序,那么您可以这样做,但我们不建议这样做。强烈建议您一步一步地完成这些最初的几章,以便在继续进行编程之前打好基础。
Unix Socket - 网络地址
在继续进行实际内容之前,让我们简单讨论一下网络地址 - IP 地址。
IP 主机地址,或更常见地称为 IP 地址,用于识别连接到互联网的主机。IP 代表互联网协议,指的是互联网整体网络架构的网络层。
IP 地址是一个 32 位的量,解释为四个 8 位的数字或八位字节。每个 IP 地址唯一地标识参与的用户网络、网络上的主机以及用户网络的类别。
IP 地址通常以点分十进制表示法编写,格式为 N1.N2.N3.N4,其中每个 Ni 都是 0 到 255 之间的十进制数(00 到 FF 十六进制)。
地址类别
IP 地址由互联网号码分配机构 (IANA) 管理和创建。有五种不同的地址类别。您可以通过检查 IP 地址的前四位来确定 IP 地址属于哪个类别。
A 类地址以0xxx开头,或十进制1 到 126。
B 类地址以10xx开头,或十进制128 到 191。
C 类地址以110x开头,或十进制192 到 223。
D 类地址以1110开头,或十进制224 到 239。
E 类地址以1111开头,或十进制240 到 254。
以01111111开头或十进制127开头的地址保留用于环回和本地机器上的内部测试[您可以测试一下:您应该始终能够 ping 127.0.0.1,它指向您自己];D 类地址保留用于多播;E 类地址保留供将来使用。它们不应用于主机地址。
示例
类别 | 最左边的位 | 起始地址 | 结束地址 |
A | 0xxx | 0.0.0.0 | 127.255.255.255 |
B | 10xx | 128.0.0.0 | 191.255.255.255 |
C | 110x | 192.0.0.0 | 223.255.255.255 |
D | 1110 | 224.0.0.0 | 239.255.255.255 |
E | 1111 | 240.0.0.0 | 255.255.255.255 |
子网划分
子网划分或子网基本上意味着将网络分支出去。它可以出于各种原因进行,例如组织中的网络、使用不同的物理介质(如以太网、FDDI、WAN 等)、保留地址空间和安全性。最常见的原因是控制网络流量。
子网划分的基本思想是将 IP 地址的主机标识符部分划分为两部分 -
- 网络地址本身内的子网地址;以及
- 子网上的主机地址。
例如,常见的 B 类地址格式为 N1.N2.S.H,其中 N1.N2 标识 B 类网络,8 位 S 字段标识子网,8 位 H 字段标识子网上的主机。
Unix Socket - 网络主机名
以数字形式表示的主机名很难记住,因此它们被称为普通名称,例如 Takshila 或 Nalanda。我们编写软件应用程序来查找与给定名称相对应的点分 IP 地址。
根据给定的字母数字主机名查找点分 IP 地址的过程称为主机名解析。
主机名解析由驻留在高容量系统上的特殊软件完成。这些系统称为域名系统 (DNS),它们保存 IP 地址及其对应普通名称的映射。
/etc/hosts 文件
主机名和 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 - 客户端-服务器模型
大多数网络应用程序都使用客户端-服务器架构,它指的是两个进程或两个应用程序彼此通信以交换某些信息。这两个进程之一充当客户端进程,另一个进程充当服务器。
客户端进程
这是通常请求信息的进程。获取响应后,此进程可能会终止或执行其他处理。
例如,Internet 浏览器充当客户端应用程序,它向 Web 服务器发送请求以获取一个 HTML 网页。
服务器进程
这是接收客户端请求的进程。在收到客户端的请求后,此进程将执行所需的处理,收集请求的信息并将其发送到请求客户端。完成后,它将准备服务另一个客户端。服务器进程始终处于警报状态并准备服务传入请求。
例如 - Web 服务器一直等待来自 Internet 浏览器的请求,并且一旦收到浏览器的任何请求,它就会获取请求的 HTML 页面并将其发送回该浏览器。
请注意,客户端需要知道服务器的地址,但服务器在建立连接之前不需要知道客户端的地址甚至存在与否。一旦建立连接,双方都可以发送和接收信息。
两层和三层架构
客户端-服务器架构有两种类型 -
两层架构 - 在此架构中,客户端直接与服务器交互。这种类型的架构可能存在一些安全漏洞和性能问题。Internet Explorer 和 Web 服务器在两层架构上工作。此处使用安全套接字层 (SSL) 解决安全问题。
三层架构 - 在这种架构中,客户端和服务器之间还存在一个软件。这个中间软件被称为“中间件”。中间件用于执行所有安全检查,并在负载过重的情况下进行负载均衡。中间件接收来自客户端的所有请求,并在执行必要的身份验证后,将该请求传递给服务器。然后服务器进行必要的处理,并将响应发送回中间件,最后中间件将此响应传递回客户端。如果要实现三层架构,则可以在 Web 服务器和 Web 浏览器之间保留任何中间件,例如 Web Logic 或 WebSphere 软件。
服务器类型
您可以拥有两种类型的服务器 -
迭代服务器 - 这是服务器最简单的形式,其中一个服务器进程为一个客户端服务,并在完成第一个请求后,接收来自另一个客户端的请求。同时,其他客户端保持等待状态。
并发服务器 - 这种类型的服务器运行多个并发进程以同时处理许多请求,因为一个进程可能需要更长时间,而另一个客户端无法等待这么长时间。在 Unix 下编写并发服务器的最简单方法是fork一个子进程来分别处理每个客户端。
如何创建客户端
建立连接的系统调用对于客户端和服务器来说略有不同,但都涉及套接字的基本构造。这两个进程都建立自己的套接字。
在客户端建立套接字涉及的步骤如下 -
使用socket()系统调用创建套接字。
使用connect()系统调用将套接字连接到服务器的地址。
发送和接收数据。有许多方法可以做到这一点,但最简单的方法是使用read()和write()系统调用。
如何创建服务器
在服务器端建立套接字涉及的步骤如下 -
使用socket()系统调用创建套接字。
使用bind()系统调用将套接字绑定到地址。对于 Internet 上的服务器套接字,地址由主机上的端口号组成。
使用listen()系统调用侦听连接。
使用accept()系统调用接受连接。此调用通常会阻塞连接,直到客户端与服务器连接。
使用read()和write()系统调用发送和接收数据。
客户端和服务器交互
以下是显示完整客户端和服务器交互的图表 -
Unix Socket - 结构
在 Unix 套接字编程中,使用各种结构来保存有关地址和端口以及其他信息的信息。大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。本章中定义的结构与 Internet 协议族相关。
sockaddr
第一个结构是sockaddr,它保存套接字信息 -
struct sockaddr { unsigned short sa_family; char sa_data[14]; };
这是一个通用的套接字地址结构,将在大多数套接字函数调用中传递。下表提供了成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
sa_family |
AF_INET AF_UNIX AF_NS AF_IMPLINK |
它表示地址族。在大多数基于 Internet 的应用程序中,我们使用 AF_INET。 |
sa_data | 特定于协议的地址 | 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]; };
以下是成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
sa_family |
AF_INET AF_UNIX AF_NS AF_IMPLINK |
它表示地址族。在大多数基于 Internet 的应用程序中,我们使用 AF_INET。 |
sin_port | 服务端口 | 网络字节序中的 16 位端口号。 |
sin_addr | IP 地址 | 网络字节序中的 32 位 IP 地址。 |
sin_zero | 未使用 | 只需将此值设置为 NULL,因为未使用。 |
in addr
此结构仅在上述结构中用作结构字段,并保存 32 位 netid/hostid。
struct in_addr { unsigned long s_addr; };
以下是成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
s_addr | 服务端口 | 网络字节序中的 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] };
以下是成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
h_name | ti.com 等 | 它是主机的官方名称。例如,tutorialspoint.com、google.com 等。 |
h_aliases | TI | 它保存主机名别名的列表。 |
h_addrtype | AF_INET | 它包含地址族,在基于 Internet 的应用程序中,它始终为 AF_INET。 |
h_length | 4 | 它保存 IP 地址的长度,对于 Internet 地址为 4。 |
h_addr_list | in_addr | 对于 Internet 地址,指针数组 h_addr_list[0]、h_addr_list[1] 等指向结构 in_addr。 |
注意 - h_addr 定义为 h_addr_list[0] 以保持向后兼容性。
servent
此特定结构用于保存与服务和关联端口相关的信息。
struct servent { char *s_name; char **s_aliases; int s_port; char *s_proto; };
以下是成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
s_name | http | 这是服务的官方名称。例如,SMTP、FTP POP3 等。 |
s_aliases | 别名 | 它保存服务别名的列表。大多数情况下,这将设置为 NULL。 |
s_port | 80 | 它将具有关联的端口号。例如,对于 HTTP,这将是 80。 |
s_proto |
TCP UDP |
它设置为使用的协议。Internet 服务是使用 TCP 或 UDP 提供的。 |
关于套接字结构的提示
套接字地址结构是每个网络程序不可分割的一部分。我们分配它们,填写它们,并将指向它们的指针传递给各种套接字函数。有时我们将指向其中一个结构的指针传递给套接字函数,它会填写内容。
我们始终通过引用传递这些结构(即,我们传递指向结构的指针,而不是结构本身),并且我们始终将结构的大小作为另一个参数传递。
当套接字函数填写结构时,长度也通过引用传递,以便其值可以由函数更新。我们称这些值为结果参数。
始终使用 memset() 或 bzero() 函数将结构变量设置为 NULL(即 '\0'),否则您的结构中可能会获得意外的垃圾值。
Unix Socket - 端口和服务
当客户端进程想要连接服务器时,客户端必须有一种识别其想要连接的服务器的方法。如果客户端知道服务器所在主机的 32 位 Internet 地址,它可以联系该主机。但是客户端如何识别在该主机上运行的特定服务器进程呢?
为了解决识别主机上运行的特定服务器进程的问题,TCP 和 UDP 都定义了一组众所周知的端口。
出于我们的目的,端口将定义为 1024 到 65535 之间的整数。这是因为所有小于 1024 的端口号都被认为是众所周知的 - 例如,telnet 使用端口 23,http 使用 80,ftp 使用 21,依此类推。
网络服务的端口分配可以在文件 /etc/services 中找到。如果您正在编写自己的服务器,则必须小心地为服务器分配端口。您应该确保此端口未分配给任何其他服务器。
通常的做法是分配大于 5000 的任何端口号。但是,许多组织编写了端口号大于 5000 的服务器。例如,Yahoo Messenger 在 5050 上运行,SIP 服务器在 5060 上运行,等等。
示例端口和服务
这是一个服务和关联端口的小列表。您可以在IANA - TCP/IP 端口分配上找到 Internet 端口和关联服务的最新列表。
服务 | 端口号 | 服务描述 |
回显 | 7 | UDP/TCP 发送回它接收到的内容。 |
丢弃 | 9 | UDP/TCP 丢弃输入。 |
日期时间 | 13 | UDP/TCP 返回 ASCII 时间。 |
字符生成 | 19 | UDP/TCP 返回字符。 |
ftp | 21 | TCP 文件传输。 |
telnet | 23 | TCP 远程登录。 |
smtp | 25 | TCP 电子邮件。 |
日期时间 | 37 | UDP/TCP 返回二进制时间。 |
tftp | 69 | UDP 简单文件传输。 |
finger | 79 | TCP 用户信息。 |
http | 80 | TCP 万维网。 |
登录 | 513 | TCP 远程登录。 |
who | 513 | UDP 关于用户的不同信息。 |
Xserver | 6000 | TCP X 窗口(注意 >1023)。 |
端口和服务函数
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; };
以下是成员字段的描述 -
属性 | 值 | 描述 |
---|---|---|
s_name | http | 它是服务的官方名称。例如,SMTP、FTP POP3 等。 |
s_aliases | 别名 | 它保存服务别名的列表。大多数情况下,它将设置为 NULL。 |
s_port | 80 | 它将具有关联的端口号。例如,对于 HTTP,它将是 80。 |
s_proto |
TCP UDP |
它设置为使用的协议。Internet 服务是使用 TCP 或 UDP 提供的。 |
Unix Socket - 网络字节序
不幸的是,并非所有计算机都以相同的顺序存储构成多字节值的字节。考虑一个由 2 个字节组成的 16 位互联网。有两种方法可以存储此值。
小端序 - 在此方案中,低位字节存储在起始地址 (A) 上,高位字节存储在下一个地址 (A + 1) 上。
大端序 - 在此方案中,高位字节存储在起始地址 (A) 上,低位字节存储在下一个地址 (A + 1) 上。
为了允许具有不同字节顺序约定的机器相互通信,Internet 协议为通过网络传输的数据指定了规范的字节顺序约定。这称为网络字节序。
在建立 Internet 套接字连接时,必须确保 sockaddr_in 结构的 sin_port 和 sin_addr 成员中的数据以网络字节序表示。
字节排序函数
在主机内部表示和网络字节序之间转换数据的例程如下 -
函数 | 描述 |
---|---|
htons() | 主机到网络短整型 |
htonl() | 主机到网络长整型 |
ntohl() | 网络到主机长整型 |
ntohs() | 网络到主机短整型 |
以下列出了一些关于这些函数的更多详细信息 -
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 位数量从网络字节序转换为主机字节序。
这些函数是宏,导致将转换源代码插入到调用程序中。在小端序机器上,代码将更改值以使其成为网络字节序。在大端序机器上,不会插入任何代码,因为不需要任何代码;这些函数定义为空。
确定主机字节序的程序
将以下代码保存在文件byteorder.c中,然后对其进行编译并在您的机器上运行。
在此示例中,我们将两字节值 0x0102 存储在短整型中,然后查看两个连续的字节,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 地址函数
Unix 提供各种函数调用来帮助您操作 IP 地址。这些函数在 ASCII 字符串(人类喜欢使用的)和网络字节序二进制值(存储在套接字地址结构中的值)之间转换 Internet 地址。
以下三个函数调用用于 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"); (...)
char *inet_ntoa(struct in_addr inaddr)
此函数调用将指定的互联网主机地址转换为互联网标准点分十进制表示法的字符串。
以下是用法示例 -
#include <arpa/inet.h> (...) char *ip; ip = inet_ntoa(dest.sin_addr); printf("IP Address is: %s\n",ip); (...)
Unix Socket - 核心函数
本章描述了编写完整 TCP 客户端和服务器所需的核心套接字函数。
下图显示了完整的客户端和服务器交互 -
socket 函数
要执行网络 I/O,进程首先必须执行的操作是调用 socket 函数,指定所需的通信协议类型和协议族等。
#include <sys/types.h> #include <sys/socket.h> int socket (int family, int type, int protocol);
此调用返回一个套接字描述符,您可以在后续的系统调用中使用它,错误时返回 -1。
参数
family - 指定协议族,是以下所示常量之一 -
族 | 描述 |
---|---|
AF_INET | IPv4 协议 |
AF_INET6 | IPv6 协议 |
AF_LOCAL | Unix 域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
本章不涵盖 IPv4 以外的其他协议。
type - 指定您想要的套接字类型。它可以取以下值之一 -
类型 | 描述 |
---|---|
SOCK_STREAM | 流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 顺序数据包套接字 |
SOCK_RAW | 原始套接字 |
protocol - 该参数应设置为以下给定的特定协议类型,或设置为 0 以选择系统为给定的 family 和 type 组合提供的默认值 -
协议 | 描述 |
---|---|
IPPROTO_TCP | TCP 传输协议 |
IPPROTO_UDP | UDP 传输协议 |
IPPROTO_SCTP | SCTP 传输协议 |
connect 函数
connect 函数由 TCP 客户端用于与 TCP 服务器建立连接。
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
如果成功连接到服务器,此调用返回 0,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
serv_addr - 是指向 struct sockaddr 的指针,其中包含目标 IP 地址和端口。
addrlen - 将其设置为 sizeof(struct sockaddr)。
bind 函数
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。
参数
sockfd - 是 socket 函数返回的套接字描述符。
my_addr - 是指向 struct sockaddr 的指针,其中包含本地 IP 地址和端口。
addrlen - 将其设置为 sizeof(struct sockaddr)。
您可以自动设置您的 IP 地址和端口
端口号为 0 表示系统将选择一个随机端口,IP 地址为 INADDR_ANY 表示服务器的 IP 地址将自动分配。
server.sin_port = 0; server.sin_addr.s_addr = INADDR_ANY;
注意 - 1024 以下的所有端口均为保留端口。您可以设置 1024 以上且 65535 以下的端口,除非这些端口正在被其他程序使用。
listen 函数
listen 函数仅由 TCP 服务器调用,它执行两个操作 -
listen 函数将未连接的套接字转换为被动套接字,指示内核应接受定向到此套接字的传入连接请求。
此函数的第二个参数指定内核应为此套接字排队的最大连接数。
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd,int backlog);
此调用在成功时返回 0,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
backlog - 是允许的连接数。
accept 函数
accept 函数由 TCP 服务器调用,以返回已完成连接队列前端的下一个已完成连接。调用的签名如下 -
#include <sys/types.h> #include <sys/socket.h> int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
此调用在成功时返回非负描述符,否则错误时返回 -1。返回的描述符假定为客户端套接字描述符,所有读写操作都将在该描述符上执行以与客户端通信。
参数
sockfd - 是 socket 函数返回的套接字描述符。
cliaddr - 是指向 struct sockaddr 的指针,其中包含客户端 IP 地址和端口。
addrlen - 将其设置为 sizeof(struct sockaddr)。
send 函数
send 函数用于通过流套接字或已连接的数据报套接字发送数据。如果要通过未连接的数据报套接字发送数据,则必须使用 sendto() 函数。
您可以使用 write() 系统调用发送数据。其签名如下 -
int send(int sockfd, const void *msg, int len, int flags);
此调用返回发送出的字节数,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
msg - 是您要发送的数据的指针。
len - 是您要发送的数据的长度(以字节为单位)。
flags - 设置为 0。
recv 函数
recv 函数用于通过流套接字或已连接的数据报套接字接收数据。如果要通过未连接的数据报套接字接收数据,则必须使用 recvfrom()。
您可以使用 read() 系统调用读取数据。此调用在辅助函数章节中进行了说明。
int recv(int sockfd, void *buf, int len, unsigned int flags);
此调用返回读取到缓冲区的字节数,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
buf - 是将信息读取到的缓冲区。
len - 是缓冲区的最大长度。
flags - 设置为 0。
sendto 函数
sendto 函数用于通过未连接的数据报套接字发送数据。其签名如下 -
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
此调用返回发送的字节数,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
msg - 是您要发送的数据的指针。
len - 是您要发送的数据的长度(以字节为单位)。
flags - 设置为 0。
to - 是指向 struct sockaddr 的指针,用于要发送数据的目标主机。
tolen - 将其设置为 sizeof(struct sockaddr)。
recvfrom 函数
recvfrom 函数用于从未连接的数据报套接字接收数据。
int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);
此调用返回读取到缓冲区的字节数,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
buf - 是将信息读取到的缓冲区。
len - 是缓冲区的最大长度。
flags - 设置为 0。
from - 是指向 struct sockaddr 的指针,用于要读取数据的目标主机。
fromlen - 将其设置为 sizeof(struct sockaddr)。
close 函数
close 函数用于关闭客户端和服务器之间的通信。其语法如下 -
int close( int sockfd );
此调用在成功时返回 0,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
shutdown 函数
shutdown 函数用于优雅地关闭客户端和服务器之间的通信。与 close 函数相比,此函数提供了更多控制。以下是 shutdown 的语法 -
int shutdown(int sockfd, int how);
此调用在成功时返回 0,否则错误时返回 -1。
参数
sockfd - 是 socket 函数返回的套接字描述符。
how - 放入以下数字之一 -
0 - 表示不允许接收,
1 - 表示不允许发送,以及
2 - 表示不允许发送和接收。当 how 设置为 2 时,它与 close() 相同。
select 函数
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。
参数
nfds - 指定要测试的文件描述符的范围。select() 函数测试范围从 0 到 nfds-1 的文件描述符
readfds - 指向类型为 fd_set 的对象,该对象在输入时指定要检查是否已准备好读取的文件描述符,在输出时指示哪些文件描述符已准备好读取。它可以为 NULL 以指示空集。
writefds - 指向类型为 fd_set 的对象,该对象在输入时指定要检查是否已准备好写入的文件描述符,在输出时指示哪些文件描述符已准备好写入。它可以为 NULL 以指示空集。
exceptfds - 指向类型为 fd_set 的对象,该对象在输入时指定要检查是否有错误条件挂起的文件描述符,在输出时指示哪些文件描述符是否有错误条件挂起。它可以为 NULL 以指示空集。
timeout - 指向 timeval 结构,该结构指定 select 调用应轮询描述符以获取可用 I/O 操作的时间长度。如果超时值为 0,则 select 将立即返回。如果 timeout 参数为 NULL,则 select 将阻塞,直到至少有一个文件/套接字句柄已准备好进行可用 I/O 操作。否则,select 将在超时中指定的时间量过去或至少有一个文件/套接字描述符已准备好进行 I/O 操作后返回。
select 函数的返回值是在文件描述符集中指定的可用于 I/O 的句柄数量。如果超时字段指定的时限已到,则 select 返回 0。以下宏用于操作文件描述符集:
FD_CLR(fd, &fdset) − 清除文件描述符集中文件描述符 fd 的位。
FD_ISSET(fd, &fdset) − 如果 fdset 指向的文件描述符集中文件描述符 fd 的位已设置,则返回非零值;否则返回 0。
FD_SET(fd, &fdset) − 设置文件描述符集中文件描述符 fd 的位。
FD_ZERO(&fdset) − 初始化文件描述符集 fdset,使其所有文件描述符的位都为零。
如果 fd 参数小于 0 或大于等于 FD_SETSIZE,则这些宏的行为未定义。
示例
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 - 辅助函数
本章描述了所有辅助函数,这些函数在进行套接字编程时使用。其他辅助函数在以下章节中描述:端口和服务以及网络字节序。
write 函数
write 函数尝试将 buf 指向的缓冲区中的 nbyte 个字节写入与打开的文件描述符 fildes 关联的文件。
您也可以使用 send() 函数将数据发送到另一个进程。
#include <unistd.h> int write(int fildes, const void *buf, int nbyte);
成功完成后,write() 返回实际写入与 fildes 关联的文件的字节数。此数字绝不超过 nbyte。否则,返回 -1。
参数
fildes − 它是由 socket 函数返回的套接字描述符。
buf − 它是指向要发送的数据的指针。
nbyte − 它是要写入的字节数。如果 nbyte 为 0,则 write() 将返回 0 并且如果文件是常规文件则没有其他结果;否则,结果未指定。
read 函数
read 函数尝试从与缓冲区 fildes 关联的文件中读取 nbyte 个字节,并将这些字节读入 buf 指向的缓冲区。
您也可以使用 recv() 函数读取另一个进程的数据。
#include <unistd.h> int read(int fildes, const void *buf, int nbyte);
成功完成后,write() 返回实际写入与 fildes 关联的文件的字节数。此数字绝不超过 nbyte。否则,返回 -1。
参数
fildes − 它是由 socket 函数返回的套接字描述符。
buf - 是将信息读取到的缓冲区。
nbyte − 它是要读取的字节数。
fork 函数
fork 函数创建一个新的进程。新进程称为子进程,它是调用进程(父进程)的精确副本。子进程从父进程继承许多属性。
#include <sys/types.h> #include <unistd.h> int fork(void);
成功完成后,fork() 返回 0 给子进程,并将子进程的进程 ID 返回给父进程。否则,返回 -1 给父进程,不创建子进程,并且 errno 被设置为指示错误。
参数
void − 表示不需要参数。
bzero 函数
bzero 函数在字符串 s 中放置 nbyte 个空字节。此函数用于将所有套接字结构设置为零值。
void bzero(void *s, int nbyte);
此函数不返回任何内容。
参数
s − 它指定要填充空字节的字符串。这将是指向套接字结构变量的指针。
nbyte − 它指定要填充空值的字节数。这将是套接字结构的大小。
bcmp 函数
bcmp 函数将字节字符串 s1 与字节字符串 s2 进行比较。假设这两个字符串都长 nbyte 个字节。
int bcmp(const void *s1, const void *s2, int nbyte);
如果两个字符串相同,则此函数返回 0,否则返回 1。当 nbyte 为 0 时,bcmp() 函数始终返回 0。
参数
s1 − 它指定要比较的第一个字符串。
s2 − 它指定要比较的第二个字符串。
nbyte − 它指定要比较的字节数。
bcopy 函数
bcopy 函数将 nbyte 个字节从字符串 s1 复制到字符串 s2。正确处理重叠的字符串。
void bcopy(const void *s1, void *s2, int nbyte);
此函数不返回任何内容。
参数
s1 − 它指定源字符串。
s2v − 它指定目标字符串。
nbyte − 它指定要复制的字节数。
memset 函数
memset 函数也用于像 bzero 一样设置结构变量。请查看下面给出的语法。
void *memset(void *s, int c, int nbyte);
此函数返回指向 void 的指针;实际上,是指向已设置内存的指针,您需要相应地将其强制转换。
参数
s − 它指定要设置的源。
c − 它指定在 nbyte 个位置上要设置的字符。
nbyte − 它指定要设置的字节数。
Unix 套接字 - 服务器示例
要使进程成为 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; }
处理多个连接
为了允许服务器处理多个同时连接,我们在上述代码中进行以下更改:
将 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 套接字 - 客户端示例
要使进程成为 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 - 总结
以下是与套接字编程相关的所有函数的列表。
端口和服务函数
Unix 提供以下函数来从 /etc/services 文件中获取服务名称。
struct servent *getservbyname(char *name, char *proto) − 此调用获取服务名称和协议名称,并返回该服务的相应端口号。
struct servent *getservbyport(int port, char *proto) − 此调用获取端口号和协议名称,并返回相应的服务名称。
字节排序函数
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 地址函数
int inet_aton (const char *strptr, struct in_addr *addrptr) − 此函数调用将指定的字符串(以 Internet 标准点分十进制表示法表示)转换为网络地址,并将地址存储在提供的结构中。转换后的地址将采用网络字节序(字节从左到右排序)。如果字符串有效则返回 1,否则返回 0。
in_addr_t inet_addr (const char *strptr) − 此函数调用将指定的字符串(以 Internet 标准点分十进制表示法表示)转换为适合用作 Internet 地址的整数值。转换后的地址将采用网络字节序(字节从左到右排序)。它返回一个 32 位二进制网络字节序 IPv4 地址,并在出错时返回 INADDR_NONE。
char *inet_ntoa (struct in_addr inaddr) − 此函数调用将指定的 Internet 主机地址转换为 Internet 标准点分十进制表示法的字符串。
套接字核心函数
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) − 此函数用于读取或写入多个套接字。
套接字辅助函数
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 函数在字符串 s 中放置 nbyte 个空字节。此函数将用于用空值设置所有套接字结构。
int bcmp (const void *s1, const void *s2, int nbyte) − bcmp 函数将字节字符串 s1 与字节字符串 s2 进行比较。假定这两个字符串都长 nbyte 字节。
void bcopy (const void *s1, void *s2, int nbyte) − bcopy 函数将 nbyte 个字节从字符串 s1 复制到字符串 s2。正确处理重叠的字符串。
void *memset(void *s, int c, int nbyte) − memset 函数也用于以与 bzero 相同的方式设置结构变量。