Unix 套接字 - 快速指南


什么是套接字?

套接字允许同一或不同机器上的两个不同进程之间进行通信。更准确地说,这是一种使用标准 Unix 文件描述符与其他计算机通信的方法。在 Unix 中,每个 I/O 操作都是通过写入或读取文件描述符来完成的。文件描述符只是与打开的文件关联的整数,它可以是网络连接、文本文件、终端或其他东西。

对于程序员来说,套接字的外观和Behave很像低级文件描述符。这是因为 read() 和 write() 等命令使用套接字的方式与使用文件和管道的方式相同。

套接字首先在 2.1BSD 中引入,随后在 4.2BSD 中完善为当前形式。现在,大多数最新的 UNIX 系统版本都提供了套接字功能。

套接字用在哪里?

Unix Socket 用于客户端-服务器应用程序框架。服务器是根据客户端请求执行某些功能的进程。大多数应用程序级协议(如 FTP、SMTP 和 POP3)都使用套接字在客户端和服务器之间建立连接,然后交换数据。

插座类型

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

假定进程仅在相同类型的套接字之间进行通信,但不存在阻止不同类型的套接字之间进行通信的限制。

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

  • 数据报套接字- 不保证网络环境中的传送。它们是无连接的,因为您不需要像流套接字那样拥有开放的连接 - 您可以使用目的地信息构建一个数据包并将其发送出去。他们使用 UDP(用户数据报协议)。

  • 原始套接字- 这些为用户提供对支持套接字抽象的底层通信协议的访问。这些套接字通常是面向数据报的,尽管它们的确切特性取决于协议提供的接口。原始套接字不适合一般用户;它们主要是为那些有兴趣开发新的通信协议或访问现有协议的一些更神秘的设施的人提供的。

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

下一步是什么?

接下来的几章旨在加强您的基础知识,并为您使用套接字编写服务器和客户端程序奠定基础。如果你想直接跳转看如何编写客户端和服务器程序,那么可以这样做,但不建议这样做。强烈建议您逐步完成最初的几章,以便在继续进行编程之前奠定基础。

Unix 套接字 - 网络地址

在我们继续讨论实际内容之前,让我们先讨论一下网络地址——IP 地址。

IP 主机地址,或更常见的是 IP 地址,用于识别连接到 Internet 的主机。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
10xx年 128.0.0.0 191.255.255.255
C 110倍 192.0.0.0 223.255.255.255
D 1110 224.0.0.0 239.255.255.255
1111 240.0.0.0 255.255.255.255

子网划分

子网划分或子网划分基本上意味着对网络进行分支。这样做可以出于多种原因,例如组织中的网络、使用不同的物理介质(例如以太网、FDDI、WAN 等)、地址空间的保存和安全性。最常见的原因是控制网络流量。

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

  • 网络地址本身内的子网地址;和
  • 子网上的主机地址。

例如,常见的B类地址格式为N1.N2.SH,其中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 页面并将其发送回该浏览器。

请注意,客户端需要知道服务器的地址,但服务器在建立连接之前不需要知道客户端的地址,甚至不需要知道客户端的存在。一旦建立连接,双方就可以发送和接收信息。

2 层和 3 层架构

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

  • 2 层架构- 在这种架构中,客户端直接与服务器交互。这种类型的架构可能存在一些安全漏洞和性能问题。Internet Explorer 和Web Server 在两层体系结构上工作。这里使用安全套接字层(SSL)解决安全问题。

  • 3 层架构- 在这种架构中,客户端和服务器之间还有一个软件。这个中间软件称为“中间件”。中间件用于在重负载的情况下执行所有安全检查和负载平衡。中间件接收来自客户端的所有请求,并在执行所需的身份验证后,将该请求传递到服务器。然后服务器进行所需的处理并将响应发送回中间件,最后中间件将此响应传递回客户端。如果您想要实现 3 层架构,那么您可以在 Web 服务器和 Web 浏览器之间保留任何中间件,例如 Web Logic 或 WebSphere 软件。

服务器类型

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

  • 迭代服务器- 这是最简单的服务器形式,其中服务器进程为一个客户端提供服务,完成第一个请求后,它接受来自另一个客户端的请求。与此同时,另一位客户仍在等待。

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

如何制作客户端

客户端和服务器建立连接的系统调用有些不同,但都涉及套接字的基本构造。两个进程都建立自己的套接字。

在客户端建立套接字所涉及的步骤如下 -

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

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

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

如何制作服务器

在服务器端建立套接字所涉及的步骤如下 -

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

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

  • 使用listen()系统调用监听连接。

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

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

客户端和服务器交互

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

套接字客户端服务器

Unix Socket - 结构

Unix 套接字编程中使用各种结构来保存有关地址和端口的信息以及其他信息。大多数套接字函数需要一个指向套接字地址结构的指针作为参数。本章定义的结构与互联网协议族相关。

套接字地址

第一个结构是sockaddr,它保存套接字信息 -

struct sockaddr {
   unsigned short   sa_family;
   char             sa_data[14];
};

这是一个通用的套接字地址结构,大多数套接字函数调用都会传递该结构。下表提供了成员字段的描述 -

属性 价值观 描述
sa_family

AF_INET

AF_UNIX

AF_NS

AF_IMPLINK

它代表一个地址族。在大多数基于 Internet 的应用程序中,我们使用 AF_INET。
萨数据 协议特定地址 14字节协议特定地址的内容根据地址类型进行解释。对于Internet家族,我们将使用端口号IP地址,它由下面定义的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。
罪恶端口 服务端口 网络字节顺序中的 16 位端口号。
罪名地址 IP地址 网络字节顺序的 32 位 IP 地址。
罪恶零 不曾用过 您只需将此值设置为 NULL,因为该值未被使用。

在地址中

该结构仅在上述结构中用作结构字段并保存32位netid/hostid。

struct in_addr {
   unsigned long s_addr;
};

这是成员字段的描述 -

属性 价值观 描述
s_地址 服务端口 网络字节顺序的 32 位 IP 地址。

主机

该结构体用于保存与主机相关的信息。

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_别名 TI 它包含主机名别名列表。
h_地址类型 AF_INET 它包含地址族,如果是基于 Internet 的应用程序,它将始终是 AF_INET。
h_长度 4 它保存IP地址的长度,对于Internet地址来说是4。
h_地址列表 输入地址 对于互联网地址,指针数组 h_addr_list[0]、h_addr_list[1] 等指向结构 in_addr。

注意- h_addr 被定义为 h_addr_list[0] 以保持向后兼容性。

仆人

这个特定的结构用于保存与服务和关联端口相关的信息。

struct servent {
   char  *s_name; 
   char  **s_aliases; 
   int   s_port;  
   char  *s_proto;
};

这是成员字段的描述 -

属性 价值观 描述
名字 http 这是该服务的正式名称。例如,SMTP、FTP POP3 等。
s_别名 别名 它保存服务别名列表。大多数时候这将被设置为 NULL。
运动 80 它将具有关联的端口号。例如,对于 HTTP,这将为 80。
s_proto

传输控制协议

UDP协议

它被设置为所使用的协议。Internet 服务使用 TCP 或 UDP 提供。

套接字结构的技巧

套接字地址结构是每个网络程序不可或缺的一部分。我们分配它们,填充它们,并将指向它们的指针传递给各种套接字函数。有时,我们将指向这些结构之一的指针传递给套接字函数,它会填充内容。

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

当套接字函数填充结构时,长度也通过引用传递,以便函数可以更新其值。我们称这些为价值-结果论证。

始终通过使用 bzero() 函数的 memset() 将结构变量设置为 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 Server 运行在 5060 上等。

端口和服务示例

这是服务和相关端口的一小部分列表。您可以在IANA - TCP/IP 端口分配中找到最新的互联网端口和相关服务列表。

服务 端口号 服务说明
回声 7 UDP/TCP 发送回其接收到的内容。
丢弃 9 UDP/TCP 丢弃输入。
白天 13 UDP/TCP 返回 ASCII 时间。
电荷 19 UDP/TCP 返回字符。
文件传输协议 21 TCP 文件传输。
远程登录 23 TCP远程登录。
邮件传输协议 25 TCP 电子邮件。
白天 37 UDP/TCP 返回二进制时间。
传输协议 69 UDP 简单文件传输。
手指 79 有关用户的 TCP 信息。
http 80 TCP 万维网。
登录 513 TCP远程登录。
WHO 513 UDP 不同的用户信息。
X服务器 6000 TCP X 窗口(NB >1023)。

端口及服务功能

Unix 提供了以下函数来从 /etc/services 文件中获取服务名称。

  • structservent *getservbyname(char *name, char *proto) - 此调用采用服务名称和协议名称,并返回该服务相应的端口号。

  • structservent *getservbyport(int port, char *proto) - 此调用采用端口号和协议名称,并返回相应的服务名称。

每个函数的返回值是一个指向以下形式的结构的指针 -

struct servent {
   char  *s_name;
   char  **s_aliases;
   int   s_port;
   char  *s_proto;
};

这是成员字段的描述 -

属性 价值观 描述
名字 http 这是该服务的正式名称。例如,SMTP、FTP POP3 等。
s_别名 别名 它保存服务别名列表。大多数时候,它会被设置为NULL。
运动 80 它将具有关联的端口号。例如,对于 HTTP,它将是 80。
s_proto

传输控制协议

UDP协议

它被设置为所使用的协议。Internet 服务使用 TCP 或 UDP 提供。

Unix Socket - 网络字节顺序

不幸的是,并非所有计算机都以相同的顺序存储组成多字节值的字节。考虑一个由 2 个字节组成的 16 位互联网。有两种方法可以存储该值。

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

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

为了允许具有不同字节顺序约定的机器相互通信,互联网协议为通过网络传输的数据指定了规范的字节顺序约定。这称为网络字节顺序。

建立 Internet 套接字连接时,必须确保 sockaddr_in 结构的 sin_port 和 sin_addr 成员中的数据以网络字节顺序表示。

字节排序函数

在主机的内部表示和网络字节顺序之间转换数据的例程如下:

功能 描述
顿斯() 主机到网络短路
htonl() 主机到网络长
恩托尔() 网络主机长
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)

此函数调用将 Internet 标准点表示法中的指定字符串转换为网络地址,并将该地址存储在提供的结构中。转换后的地址将采用网络字节顺序(字节从左到右排序)。如果字符串有效则返回 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)

此函数调用将 Internet 标准点表示法中的指定字符串转换为适合用作 Internet 地址的整数值。转换后的地址将采用网络字节顺序(字节从左到右排序)。它返回一个 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)

此函数调用将指定的 Internet 主机地址转换为 Internet 标准点表示法的字符串。

以下是使用示例 -

#include <arpa/inet.h>

(...)

   char *ip;
   
   ip = inet_ntoa(dest.sin_addr);
   
   printf("IP Address is: %s\n",ip);
   
(...)

Unix Socket - 核心功能

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

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

套接字客户端服务器

套接字功能

要执行网络 I/O,进程必须做的第一件事是调用套接字函数,指定所需的通信协议类型和协议族等。

#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_路由 路由套接字
AF_KEY 凯特插座

本章不涉及除 IPv4 之外的其他协议。

type - 它指定您想要的套接字类型。它可以采用以下值之一 -

类型 描述
SOCK_STREAM 流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 顺序数据包套接字
SOCK_RAW 原始套接字

协议- 参数应设置为下面给出的特定协议类型,或 0 为给定的系列和类型组合选择系统的默认值 -

协议 描述
IP协议_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

连接函数_

connect函数用于TCP客户端与TCP服务器建立连接。

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

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

如果成功连接到服务器,则此调用返回 0,否则返回 -1 错误。

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • serv_addr - 它是一个指向 struct sockaddr 的指针,其中包含目标 IP 地址和端口。

  • addrlen - 将其设置为 sizeof(struct sockaddr)。

绑定函数_

绑定函数本地协议地址分配给套接字。对于 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 - 它是套接字函数返回的套接字描述符。

  • 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;

- 1024 以下的所有端口均被保留。您可以将端口设置为高于 1024 且低于 65535,除非它们被其他程序使用。

监听功能_

监听函数仅由 TCP 服务器调用,它执行两个操作-

  • 监听函数将未连接的套接字转换为被动套接字,表明内核应该接受指向该套接字的传入连接请求。

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

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

int listen(int sockfd,int backlog);

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • backlog - 这是允许的连接数。

接受函数_

TCP 服务器调用accept 函数,从已完成的连接队列的前面返回下一个已完成的连接调用的签名如下 -

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

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

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • cliaddr - 它是指向包含客户端 IP 地址和端口的 struct sockaddr 的指针。

  • addrlen - 将其设置为 sizeof(struct sockaddr)。

发送函数_

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

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

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

该调用返回发送出去的字节数,否则错误时返回-1。

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • msg - 它是指向您要发送的数据的指针。

  • len - 这是您要发送的数据的长度(以字节为单位)。

  • flags - 设置为 0。

接收函数_

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

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

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

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • buf - 读取信息的缓冲区。

  • len - 缓冲区的最大长度。

  • flags - 设置为 0。

发送函数

sendto函数用于通过 UNCONNECTED 数据报套接字发送数据。其签名如下 -

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

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • msg - 它是指向您要发送的数据的指针。

  • len - 这是您要发送的数据的长度(以字节为单位)。

  • flags - 设置为 0。

  • to - 它是指向必须发送数据的主机的 struct sockaddr 的指针。

  • tolen - 将其设置为 sizeof(struct sockaddr)。

接收函数_

recvfrom函数用于从 UNCONNECTED 数据报套接字接收数据

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

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • buf - 读取信息的缓冲区。

  • len - 缓冲区的最大长度。

  • flags - 设置为 0。

  • from - 它是指向必须读取数据的主机的 struct sockaddr 的指针。

  • fromlen - 将其设置为 sizeof(struct sockaddr)。

关闭函数_

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

int close( int sockfd );

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

关机功能_

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

int shutdown(int sockfd, int how);

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

参数

  • sockfd - 它是套接字函数返回的套接字描述符。

  • 如何- 输入其中一个数字 -

    • 0 - 表示不允许接收,

    • 1 - 表示不允许发送,并且

    • 2 - 表示不允许发送和接收。当how设置为2时,与close()是一样的。

选择功能_

选择函数指示哪个指定的文件描述符已准备好读取、准备好写入或有待处理的错误条件

当应用程序调用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将立即返回。如果超时参数为 NULL,则 select 将阻塞,直到至少一个文件/套接字句柄准备好进行可用的 I/O 操作。否则,select将在超时时间过后或至少一个文件/套接字描述符已准备好进行 I/O 操作时返回。

select 的返回值是文件描述符集中指定的准备进行 I/O 的句柄数。如果达到超时字段指定的时间限制,则选择返回 0。存在以下宏用于操作文件描述符集 -

  • FD_CLR(fd, &fdset) - 清除文件描述符集 fdset 中文件描述符 fd 的位

  • FD_ISSET(fd, &fdset) - 如果在fdset指向的文件描述符集中设置了文件描述符fd的位,则返回非零值,否则返回 0。

  • FD_SET(fd, &fdset) - 设置文件描述符集 fdset 中文件描述符 fd 的位。

  • FD_ZERO(&fdset) - 初始化文件描述符集 fdset,使所有文件描述符都具有零位。

如果 fd 参数小于 0 或大于或等于 FD_SETSIZE,则这些宏的Behave未定义。

例子

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函数尝试将buf指向的缓冲区中的 nbyte 字节写入与打开的文件描述符fildes关联的文件。

您还可以使用send()函数将数据发送到另一个进程。

#include <unistd.h>

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

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

参数

  • fildes - 它是套接字函数返回的套接字描述符。

  • buf - 它是指向您要发送的数据的指针。

  • nbyte - 要写入的字节数。如果nbyte为0,如果文件是普通文件,则write()将返回0并且没有其他结果;否则,结果未指定。

读取函数_

read函数尝试从与缓冲区 fildes 关联的文件中读取 nbyte 字节到 buf 指向的缓冲区中

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

#include <unistd.h>

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

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

参数

  • fildes - 它是套接字函数返回的套接字描述符。

  • buf - 读取信息的缓冲区。

  • nbyte - 要读取的字节数。

叉子功能_

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

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

int fork(void);

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

参数

  • void - 这意味着不需要参数。

bzero函数_

bzero函数将nbyte空字节放入字符串s中。该函数用于将所有套接字结构设置为空值。

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 Socket - 服务器示例

要使进程成为 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 Socket - 客户端示例

要使进程成为 TCP 客户端,您需要按照下面给出的步骤 +minus ;

  • 使用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 - 总结

这里列出了所有与socket编程相关的函数。

端口及服务功能

Unix 提供了以下函数来从 /etc/services 文件中获取服务名称。

  • structservent *getservbyname(char *name, char *proto) - 此调用采用服务名称和协议名称,并返回该服务相应的端口号。

  • structservent *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) - TCP 客户端使用 connect 函数与 TCP 服务器建立连接。如果成功连接到服务器,此调用将返回 0,否则返回 -1。

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

  • int Listen(int sockfd, int backlog) - 监听函数仅由 TCP 服务器调用来监听客户端请求。成功时此调用返回 0,否则返回 -1。

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

  • int send(int sockfd, const void *msg, int len, int flags) - send 函数用于通过流套接字或 CONNECTED 数据报套接字发送数据。该调用返回发送出去的字节数,否则返回-1。

  • int recv (int sockfd, void *buf, int len, unsigned int flags) - recv 函数用于通过流套接字或 CONNECTED 数据报套接字接收数据。此调用返回读入缓冲区的字节数,否则返回 -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)