Unix Sockets 简明教程

Unix Socket - Quick Guide

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

用户可以使用四种类型的套接字。前两种最常用,后两种很少用。

通常认为进程只能在同类型套接字之间进行通信,但是没有规定阻止不同类型套接字之间的通信。

  1. Stream Sockets − 在网络环境中保证交付。如果您通过流套接字发送三个项目“A、B、C”,它们将按相同顺序到达 − “A、B、C”。这些套接字使用 TCP(传输控制协议)进行数据传输。如果无法交付,发件人将收到一个错误指示器。数据记录没有任何边界。

  2. Datagram Sockets − 在网络环境中不保证交付。它们是无连接的,因为您不需要像流套接字那样保持开放连接 − 只需构建一个包含目标信息的数据包然后发送出去即可。它们使用 UDP(用户数据报协议)。

  3. Raw Sockets − 这些套接字为用户提供了访问基础通信协议的功能,这些协议支持套接字抽象。这些套接字通常面向数据报,尽管它们的具体特征取决于协议提供的接口。原始套接字并非面向普通用户;它们主要是为那些有兴趣开发新的通信协议或想要访问现有协议的某些更隐晦功能的人提供的。

  4. Sequenced Packet Sockets − 它们类似于流套接字,但保留了记录边界。此接口仅作为网络系统 (NS) 套接字抽象的一部分提供,并且在大多数重要的 NS 应用程序中非常重要。顺序数据包套接字允许用户操作数据包或一组数据包上的顺序数据包协议 (SPP) 或互联网数据报协议 (IDP) 标头,方法是随要发送的任何数据编写一个原型标头,或者指定一个默认标头以在所有传出数据中使用,并且允许用户接收传入数据包的标头。

What is Next?

接下来的几章旨在加强你的基础知识,在你使用套接字编写服务器和客户端程序之前奠定基础。如果您想直接跳到如何编写客户端和服务器程序,您可以这样做,但这不是推荐做法。强烈建议你一步一步地进行,完成这些最初的几章,在你开始编程之前打好基础。

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 地址属于哪个类。

  1. Class A 地址以 0xxx ,或 1 to 126 十进制开头。

  2. Class B 地址以 10xx ,或 128 to 191 十进制开头。

  3. Class C 地址以 110x ,或 192 to 223 十进制开头。

  4. Class D 地址以 1110 ,或 224 to 239 十进制开头。

  5. Class E 地址以 1111240 to 254 十进制开头。

01111111127 十进制开头的地址保留用于环回和本地计算机的内部测试 [您可以测试这一点:您应该始终能够 ping 127.0.0.1 ,它指向您自己];D 类地址保留用于多播;E 类地址保留供将来使用。不应将其用于主机地址。

Example

Class

Leftmost bits

Start address

Finish address

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

Subnetting

子网划分为或子网划定基本上意味着分支网络。出于各种原因可以进行此操作,例如组织中的网络、使用不同的物理媒体(例如以太网、FDDI、WAN 等)、保留地址空间和安全性。最常见的原因是控制网络流量。

子网划分的基本思路是将 IP 地址的主机标识符部分划分为两部分——

  1. 网络地址本身内的子网地址;以及

  2. 子网上的主机地址。

例如,常见的 B 类地址格式为 N1.N2.S.H,其中 N1.N2 标识 B 类网络,8 位 S 字段标识子网,8 位 H 字段标识子网上的主机。

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

有两种类型的客户端-服务器架构 −

  1. 2-tier architecture − 在此架构中,客户端直接与服务器交互。这种类型的架构可能存在一些安全漏洞和性能问题。Internet Explorer 和 Web 服务器使用两层架构。在此,使用安全套接字层 (SSL) 来解决安全问题。

  2. 3-tier architectures − 在此架构中,另一个软件位于客户端和服务器之间。此中间软件称为“中间件”。使用中间件来执行所有安全检查并在负载量大的情况下进行负载平衡。中间件从客户端接收所有请求,并执行必需的身份验证后,将请求传递给服务器。然后,服务器执行必需的处理并将响应发送回中间件,最终中间件将此响应传递回客户端。如果您想实现 3 层架构,那么您可以在 Web 服务器和 Web 浏览器之间放置任何中间件(如 Web Logic 或 WebSphere 软件)。

Types of Server

您可以有两种类型的服务器 −

  1. Iterative Server − 这是服务器的最简单形式,其中一个服务器进程服务一个客户端,并且在完成第一个请求后,接收来自另一个客户端的请求。同时,另一个客户端保持等待状态。

  2. Concurrent Servers − 这种类型的服务器运行多个并发进程以一次处理多个请求,因为一个进程可能需要更长时间,并且另一个客户端不能等待这么长时间。在 Unix 下编写并发服务器最简单的方法是为每个客户端派生一个子进程以单独处理。

How to Make Client

用于建立连接的系统调用对于客户端和服务器来说是不同的,但两者都涉及套接字的基本结构。这两个进程都建立自己的套接字。

在客户端建立套接字涉及以下步骤 −

  1. 使用 socket() 系统调用创建一个套接字。

  2. 使用 connect() 系统调用将套接字连接到服务器的地址。

  3. 发送和接收数据。有许多方法可以做到这一点,但最简单的方法是使用 read()write() 系统调用。

How to make a Server

在服务器端建立套接字涉及以下步骤 −

  1. 使用 socket() 系统调用创建一个套接字。

  2. 使用 bind() 系统调用将套接字绑定到一个地址。对于互联网上的服务器套接字,地址包含主机上的端口号。

  3. 使用 listen() 系统调用侦听连接。此调用通常会阻塞连接,直到客户端连接到服务器。

  4. 使用 accept() 系统调用接受连接。此调用通常会阻塞连接,直到客户端连接到服务器。

  5. 使用 read()write() 系统调用发送和接收数据。

Client and Server Interaction

以下是显示完整客户端和服务器交互的图表 −

socket client server

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 提供。

Tips on Socket Structures

套接字地址结构是每个网络程序的组成部分。我们对其进行分配、填写,并将其指针传递给各种套接字函数。有时我们会向套接字函数传递指向其中一个结构的指针,它会填写内容。

我们总是通过引用方式传递这些结构(即,我们传递一个指向该结构的指针,而不是结构本身),并且我们总是将结构的大小作为一个其他参数传递。

当套接字函数填写一个结构时,长度也会通过引用进行传递,以便函数可以更新其值。我们将这些值称为值结果参数。

始终使用 memset()(对于 bzero() 函数)将结构变量设置为 NULL(即 '\0'),否则您的结构中可能会出现意外的垃圾值。

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 文件中获取服务名称。

  1. struct servent *getservbyname(char *name, char *proto) − 该调用获取服务名称和协议名称,并为此服务返回相应端口号。

  2. 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 位互联网。有两种方法来存储此值。

  1. Little Endian − 在此方案中,低位字节存储在起始地址 (A) 上,而高位字节存储在下一个地址 (A + 1) 上。

  2. 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

下面列出了一些有关这些函数的更多详细信息 −

  1. unsigned short htons(unsigned short hostshort) − 此函数将 16 位(2 字节)量从主机字节顺序转换为网络字节顺序。

  2. unsigned long htonl(unsigned long hostlong) − 此函数将 32 位(4 字节)量从主机字节顺序转换为网络字节顺序。

  3. unsigned short ntohs(unsigned short netshort) − 此函数将 16 位(2 字节)量从网络字节顺序转换为主机字节顺序。

  4. 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 寻址:

  1. int inet_aton(const char *strptr, struct in_addr *addrptr)

  2. in_addr_t inet_addr(const char *strptr)

  3. 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 - Core Functions

本章介绍了编写完整的 TCP 客户端和服务器所需的核心套接字函数。

下图显示了完整的客户端和服务器交互 -

socket client server

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。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. serv_addr − 指向包含目标 IP 地址和端口的 struct sockaddr。

  3. addrlen − 将其设置为 sizeof(struct sockaddr)。

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

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. my_addr − 指向包含本地 IP 地址和端口的 struct sockaddr。

  3. 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 服务器调用,并且它执行两个操作 −

  1. listen 函数将未连接的套接字转换为一个被动套接字,表示内核应该接受定向到此套接字的传入连接请求。

  2. 传递给此函数的第二个参数指定内核应该为该套接字排队的最大连接数。

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd,int backlog);

此调用成功时返回 0,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. backlog − 为允许的连接数。

The accept Function

accept 函数由 TCP 服务器调用,以从已完成连接队列最前端返回下一个已完成连接。调用的签名如下:

#include <sys/types.h>
#include <sys/socket.h>

int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

此调用成功时返回非负描述符,否则在错误时返回 -1。假定返回的描述符为客户端套接字描述符,并且所有读写操作都将在此描述符上执行以与客户端进行通信。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. cliaddr − 为指向包含客户端 IP 地址和端口的 struct sockaddr 的指针。

  3. addrlen − 将其设置为 sizeof(struct sockaddr)。

The send Function

send 函数用于通过流套接字或已连接的数据报套接字发送数据。如果您想通过未连接的数据报套接字发送数据,则必须使用 sendto() 函数。

您可以使用 write() 系统调用来发送数据。其签名如下:

int send(int sockfd, const void *msg, int len, int flags);

此调用返回发送的字节数,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. msg − 为指向要发送的数据的指针。

  3. len − 为要发送的数据的长度(以字节为单位)。

  4. flags − 设置为 0。

The recv Function

recv 函数用于通过流套接字或已连接的数据报套接字接收数据。如果您想通过未连接的数据报套接字接收数据,则必须使用 recvfrom()。

您可以使用 read() 系统调用来读取数据。此调用在帮助程序函数章节中进行了解释。

int recv(int sockfd, void *buf, int len, unsigned int flags);

此调用返回读入缓冲区的字节数,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. buf − 为读取信息的缓冲区。

  3. len − 为缓冲区的最大长度。

  4. flags − 设置为 0。

The sendto Function

sendto 函数用于通过未连接的数据报套接字发送数据。其签名如下:

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

此调用返回发送的字节数,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. msg − 为指向要发送的数据的指针。

  3. len − 为要发送的数据的长度(以字节为单位)。

  4. flags − 设置为 0。

  5. to − 为指向数据将发送到的主机的 struct sockaddr。

  6. tolen − 将其设置为 sizeof(struct sockaddr)。

The recvfrom Function

recvfrom 函数用来从未连接的数据报套接字接收数据。

int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);

此调用返回读取到缓冲区中的字节数,否则在出错时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. buf − 为读取信息的缓冲区。

  3. len − 为缓冲区的最大长度。

  4. flags − 设置为 0。

  5. from − 这是数据必须从中读取的主机的 struct sockaddr 的指针。

  6. fromlen − 将其设置为 sizeof(struct sockaddr)。

The close Function

close 函数用来关闭客户端和服务器之间的通信。其语法如下:

int close( int sockfd );

此调用成功时返回 0,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

The shutdown Function

shutdown 函数用来优雅地关闭客户端和服务器之间的通信。与 close 函数相比,此函数提供更多控制。以下是 shutdown 的语法:

int shutdown(int sockfd, int how);

此调用成功时返回 0,否则在错误时返回 -1。

Parameters

  1. sockfd − 由 socket 函数返回的套接字描述符。

  2. how − 放入以下某个数字: 0 − 表示不允许接收, 1 − 表示不允许发送, 2 − 表示不允许发送和接收。当 how 设置为 2 时,它与 close() 相同。

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

  1. nfds − 它指定要测试的文件描述符范围。select() 函数测试范围为 0 到 nfds-1 的文件描述符

  2. readfds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否已准备好进行读取,并且在输出时,它指示哪些文件描述符已准备好进行读取。它可以为 NULL,以指示一个空集。

  3. writefds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否已准备好进行写入,并且在输出时,它指示哪些文件描述符已准备好进行写入。它可以为 NULL,以指示一个空集。

  4. exceptfds − 在输入时,它指向 type fd_set 的一个对象,该对象指定要检查的文件描述符是否有待处理的错误条件,并且在输出时,它指示哪些文件描述符有待处理的错误条件。它可以为 NULL,以指示一个空集。

  5. timeout − 它指向一个 timeval struct,该 struct 指定 select 调用应该轮询描述符多长时间,以获得一个可用的 I/O 操作。如果超时值为 0,则 select 将立即返回。如果 timeout 参数为 NULL,则 select 将阻塞,直至至少有一个文件/套接字句柄已准备好进行可用的 I/O 操作。否则,select 将在 timeout 中的时间量经过或至少有一个文件/套接字描述符已准备好进行 I/O 操作后返回。

来自 select 的返回值是文件描述符集中指定的文件句柄数,这些句柄已准备好进行 I/O。如果 timeout 字段指定的时间限制达到,select 返回 0。以下宏用于操作文件描述符集:

  1. FD_CLR(fd, &amp;fdset) − 清除文件描述符集 fdset 中文件描述符 fd 的位。

  2. FD_ISSET(fd, &amp;fdset) − 如果文件描述符集中由fdset 指向的文件描述符的位被设置,则返回一个非零值;否则返回 0。

  3. FD_SET(fd, &amp;fdset) − 在文件描述符集中 fdset 中设置文件描述符 fd 的位。

  4. FD_ZERO(&amp;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。

Parameters

  1. fildes − 它是 socket 函数返回的套接字描述符。

  2. buf − 它是指向要发送的数据的指针。

  3. nbyte − 它是要写入的字节数。如果 nbyte 为 0,则如果文件是常规文件,write() 将返回 0 并且没有其他结果;否则,结果未指定。

The read Function

read 函数尝试从与缓冲区 fildes 关联的文件读取 nbyte 字节,并将其读入 buf 指向的缓冲区。

您还可以使用 recv() 函数读取另一个进程的数据。

#include <unistd.h>

int read(int fildes, const void *buf, int nbyte);

成功完成后,write() 返回实际写入与 fildes 关联的文件的字节数。此数字永远不会大于 nbyte。否则,返回 -1。

Parameters

  1. fildes − 它是 socket 函数返回的套接字描述符。

  2. buf − 为读取信息的缓冲区。

  3. nbyte − 它是要读取的字节数。

The fork Function

fork 函数创建一个新进程。称为子进程的新进程将是调用进程(父进程)的确切副本。子进程从父进程继承许多属性。

#include <sys/types.h>
#include <unistd.h>

int fork(void);

成功完成后,fork() 将 0 返回给子进程,并将子进程的进程 ID 返回给父进程。否则,返回 -1 给父进程,不会创建子进程,并设置 errno 指示错误。

Parameters

  1. void − 表示不需要参数。

The bzero Function

bzero 函数在字符串 s 中放置 nbyte 个空字节。此函数用于使用空值设置所有套接字结构。

void bzero(void *s, int nbyte);

此函数不会返回任何内容。

Parameters

  1. s − 它指定了必须用空字节填充的字符串。这将是套接字结构变量的一个点。

  2. nbyte − 它指定了要用空值填充的字节数。这将是套接字结构的大小。

The bcmp Function

bcmp 函数将字节字符串 s1 与字节字符串 s2 进行比较。假设这两个字符串都是 nbyte 字节长。

int bcmp(const void *s1, const void *s2, int nbyte);

如果这两个字符串相同,则此函数返回 0,否则返回 1。如果 nbyte 为 0,bcmp() 函数始终返回 0。

Parameters

  1. s1 − 它指定要比较的第一个字符串。

  2. s2 − 它指定要比较的第二个字符串。

  3. nbyte − 它指定要比较的字节数。

The bcopy Function

bcopy 函数将 nbyte 字节从字符串 s1 复制到字符串 s2。重叠字符串得到了正确的处理。

void bcopy(const void *s1, void *s2, int nbyte);

此函数不会返回任何内容。

Parameters

  1. s1 − 它指定源字符串。

  2. s2v − 它指定目标字符串。

  3. nbyte − 它指定要复制的字节数。

The memset Function

memset 函数还用于像 bzero 一样设置结构变量。请看下面给出的语法。

void *memset(void *s, int c, int nbyte);

此函数返回一个指向 void 的指针;实际上是一个指向已设置内存的指针,并且需要相应地对其进行转换。

Parameters

  1. s − 它指定要设置的源。

  2. c − 它指定要设置在 nbyte 位置的字符。

  3. nbyte − 它指定要设置的字节数。

Unix Socket - Server Examples

若要使进程成为 TCP 服务器,您需要遵循以下步骤 −

  1. 使用 socket() 系统调用创建一个套接字。

  2. 使用 bind() 系统调用将套接字绑定到一个地址。对于 Internet 上的服务器套接字来说,一个地址由主机上的端口号组成。

  3. 用 listen() 系统调用侦听连接。

  4. 使用 accept() 系统调用接受连接。该调用通常会阻塞,直到客户端连接到服务器。

  5. 使用 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

为了允许服务器处理多个同时连接,我们在上述代码中进行了以下更改 -

  1. 将 accept 语句和以下代码放在一个无限循环中。

  2. 在建立连接之后,调用 fork() 创建一个新进程。

  3. 子进程将关闭 sockfd 并调用 doprocessing 函数,将新的套接字文件描述符作为参数传递。当 doprocessing() 返回表示两个进程已完成其对话后,此进程便退出。

  4. 父进程关闭 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 客户端,您需要按照以下步骤操作:

  1. 使用 socket() 系统调用创建一个套接字。

  2. 使用 connect() 系统调用将套接字连接到服务器的地址。

  3. 发送和接收数据。有很多方法可以做到这一点,但是最简单的方法是使用 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 文件中获取服务名称。

  1. struct servent *getservbyname(char *name, char *proto) - 此调用使用服务名称和协议名称,并返回该服务的对应端口号。

  2. struct servent *getservbyport(int port, char *proto) - 此调用使用端口号和协议名称,并返回对应服务名称。

Byte Ordering Functions

  1. unsigned short htons (unsigned short hostshort) - 此函数将 16 位(2 字节)量从主机字节顺序转换为网络字节顺序。

  2. unsigned long htonl (unsigned long hostlong) - 此函数将 32 位(4 字节)数量从主机字节顺序转换为网络字节顺序。

  3. unsigned short ntohs (unsigned short netshort) - 此函数将 16 位(2 字节)数量从网络字节顺序转换为主机字节顺序。

  4. unsigned long ntohl (unsigned long netlong) - 此函数将 32 位数量从网络字节顺序转换为主机字节顺序。

IP Address Functions

  1. int inet_aton (const char *strptr, struct in_addr *addrptr) - 此函数调用将互联网标准点符号表示形式中的指定字符串转换为网络地址,并将地址存储在指定结构中。转换后的地址将采用网络字节顺序(字节从左到右排序)。如果字符串有效,则它返回 1,如果出错,则返回 0。

  2. in_addr_t inet_addr (const char *strptr) - 此函数调用将互联网标准点符号表示形式中的指定字符串转换为适合用作互联网地址的整数值。转换后的地址将采用网络字节顺序(字节从左到右排序)。它返回一个 32 位二进制网络字节顺序 IPv4 地址,如果出错,则返回 INADDR_NONE。

  3. char *inet_ntoa (struct in_addr inaddr) - 此函数调用将指定的互联网主机地址转换为互联网标准点符号表示形式中的字符串。

Socket Core Functions

  1. int socket (int family, int type, int protocol) - 此调用返回一个套接字描述符,你可以在稍后的系统调用中使用它,或者在出错时它会给你 -1。

  2. int connect (int sockfd, struct sockaddr *serv_addr, int addrlen) - connect 函数由 TCP 客户端用于建立与 TCP 服务器的连接。如果此调用成功连接到服务器,则它返回 0,否则它返回 -1。

  3. int bind(int sockfd, struct sockaddr *my_addr,int addrlen) - bind 函数向套接字分配本地协议地址。如果此调用成功绑定到地址,则它返回 0,否则它返回 -1。

  4. int listen(int sockfd, int backlog) - listen 函数仅由 TCP 服务器调用,以侦听客户端请求。此调用成功时返回 0,否则它返回 -1。

  5. int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen) - accept 函数由 TCP 服务器调用,以接受客户端请求并建立实际连接。此调用成功时返回一个非负描述符,否则它返回 -1。

  6. int send(int sockfd, const void *msg, int len, int flags) - send 函数用于通过流套接字或已连接的数据报套接字发送数据。此调用返回已发送出的字节数,否则它返回 -1。

  7. int recv (int sockfd, void *buf, int len, unsigned int flags) - recv 函数用于通过流套接字或已连接的数据报套接字接收数据。此调用返回已读入缓冲区的字节数,否则它返回 -1。

  8. int sendto (int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen) - sendto 函数用于通过未连接的数据报套接字发送数据。此调用返回已发送出的字节数,否则它返回 -1。

  9. int recvfrom (int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen) - recvfrom 函数用于从未连接的数据报套接字接收数据。此调用返回已读入缓冲区的字节数,否则它返回 -1。

  10. int close (int sockfd) - close 函数用于关闭客户端和服务器之间的通信。此调用成功时返回 0,否则它返回 -1。

  11. int shutdown (int sockfd, int how) - shutdown 函数用于优雅地关闭客户端和服务器之间的通信。与 close 函数相比,此函数提供了更多控制。此调用成功时返回 0,否则返回 -1。

  12. int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout) - 此函数用于读写多个套接字。

Socket Helper Functions

  1. int write (int fildes, const void *buf, int nbyte) - write 函数尝试将 buf 指向的缓冲区中的 nbyte 字节写入与打开的文件描述符 fildes 关联的文件。如果成功完成,write() 将返回实际写入与 fildes 关联的文件的字节数。此数量永远不会大于 nbyte。否则,将返回 -1。

  2. int read (int fildes, const void *buf, int nbyte) − read 函数尝试从与开放的文件描述符关联的文件 fildes,读取 nbyte 字节到 buf 指向的缓冲器内。在成功完成时,write() 返回实际写入到与 fildes 关联的文件内的字节数。此数字永远不会大于 nbyte。否则,将返回 -1。

  3. int fork (void) − fork 函数创建一个新进程。新进程称为子进程,将成为调用进程(父进程)的确切副本。

  4. void bzero (void *s, int nbyte) − bzero 函数放置 nbyte 空值到字符串 s 中。此函数将用来设置带空值的全部套接字结构。

  5. int bcmp (const void *s1, const void *s2, int nbyte) − bcmp 函数比较字节字符串 s1 和字节字符串 s2。认为这两个字符串都是 nbyte 字节长。

  6. void bcopy (const void *s1, void *s2, int nbyte) − bcopy 函数从字符串 s1 复制 nbyte 字节到字符串 s2 中。重叠字符串正确地被处理。

  7. void *memset(void *s, int c, int nbyte) − memset 函数也被用来设置结构变量比如 bzero。