0%

Linux程序设计(三)

Linux课程笔记

对应PPT第4章:Sockets

Socket Connections

What is a Socket

套接字是一种通信机制,它允许在本地,单机或跨网络开发客户端/服务器系统。
套接字的创建和使用与管道不同,因为它们在客户端和服务器之间有明显的区别。
套接字机制可以实现连接到单个服务器的多个客户端。

Connections

首先,服务器应用程序创建一个套接字。
接下来,服务器进程为套接字命名。套接字使用系统调用绑定命名。

  • 本地套接字在Linux文件系统中有一个文件名,通常在 /tmp/usr/tmp 中找到。
  • 对于网络套接字,文件名将是与客户端可以连接到的特定网络相关的服务标识符(端口号/访问点)。

系统调用,侦听,为传入连接创建队列。
服务器可以使用系统调用接受接受它们。
当服务器调用accept时,将创建一个与指定套接字不同的新套接字。

  • 此新套接字仅用于与此特定客户端的通信。
  • 命名的套接字将保留,以用于其他客户端的进一步连接。

客户端通过调用套接字来创建未命名的套接字。然后,它使用服务器的命名套接字作为地址,调用connect建立与服务器的连接。
一旦建立,套接字就可以像低级文件描述符一样使用,提供双向数据通信。

Socket Attributes

套接字的特征在于三个属性:domain(域),type(类型)和protocol(协议),并且有一个地址用作其名称。
地址的格式取决于域(也称为协议系列)而有所不同。
每个协议族都可以使用一个或多个地址族来定义地址格式。

  1. Socket Domains

    域指定套接字通信将使用的网络介质。
    最常见的套接字域是 AF_INET ,它是指Internet网络。

    • 底层协议,Internet协议(IP

    UNIX文件系统域 AF_UNIX 可由基于可能未联网的单台计算机的套接字使用。

    • 基本协议是文件输入/输出,地址是绝对文件名。

    可能使用的其他域包括用于基于ISO标准协议的网络的AF_ISO和用于Xerox网络系统的AF_XNS

  2. Socket Types

    SOCK_STREAM (流套接字)

    • 流套接字提供的连接是有序的和可靠的双向字节流。
    • SOCK_STREAM 类型指定的流套接字是通过 TCP/IP 连接在 AF_INET 域中实现的。

    SOCK_DGRAM (数据报套接字)

    • SOCK_DGRAM 类型指定的数据报套接字无法建立和维护连接。
    • 数据报套接字通过 UDP/IP 连接在 AF_INET 域中实现。
  3. Socket Protocols

    如果底层传输机制允许多个协议提供请求的套接字类型,则可以为套接字选择特定的协议。
    我们将集中讨论UNIX网络和文件系统套接字,这不需要您选择默认协议以外的协议。

Creating a Socket

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

套接字系统调用创建一个套接字并返回可用于访问该套接字的描述符。
域参数指定地址族,类型参数指定与此套接字一起使用的通信(通信)的类型,协议指定要使用的协议。

Domains Description
AF_UNIX UNIX internal (file system sockets)
AF_INET ARPA Internet protocols (UNIX network sockets)
AF_ISO ISO standard protocols
AF_NS Xerox Network System protocols
AF_IPX Novell IPX protocol
AF_APPLETALK Appletalk DDS

最常见的套接字域是 AF_UNIX(用于通过UNIX和Linux文件系统实现的本地套接字)和 AF_INET (用于UNIX网络套接字)。

套接字参数类型指定要用于新套接字的通信特性。可能的值包括 SOCK_STREAMSOCK_DGRAM
用于通信的协议通常由套接字类型和域确定。
套接字系统调用返回一个描述符,并且当套接字已连接到另一个端点套接字时,您可以将读写系统调用与描述符一起使用,以在套接字上发送和接收数据。
close系统调用用于结束套接字连接。

Socket Addresses

每个套接字域都需要其自己的地址格式。
对于 AF_UNIX 套接字,该地址由 sys/un.h 包含文件中定义的结构 sockaddr_un 描述。

1
2
3
4
struct socket_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path; /* pathname */
};

AF_INET 域中,使用 netinet/in.h 中定义的称为 sockaddr_in 的结构指定地址,该结构至少包含以下成员:

1
2
3
4
5
struct sockaddr_in {
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* Port number */
short in_addr sin_addr; /* Internet address */
};

IP地址结构 in_addr 定义如下:

1
2
3
struct in_addr {
unsigned long int s_addr;
};

为了使套接字可供其他进程使用,服务器程序需要给套接字命名。

  • AF_UNIX 套接字与文件系统路径名相关联,
  • AF_INET 套接字与 IP 端口号关联。
1
2
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);

bind系统调用将在参数address中指定的地址分配给与文件描述符套接字关联的未命名套接字。

地址结构的长度作为 address_len 传递。 地址的长度和格式取决于地址系列。
在bind调用中,需要将特定的地址结构指针转换为通用地址类型(struct sockaddr *)。
成功完成后,bind返回0。如果失败,则返回-1并设置 errno

Creating a Socket Queue

若要接受套接字上的传入连接,服务器程序必须创建一个队列来存储未处理请求。 它使用侦听系统调用执行此操作。

1
2
#include <sys/socket.h>
int listen(int socket, int backlog);

listen将队列长度设置为积压。 通过侦听提供此机制,以允许在服务器程序忙于处理先前的客户端时将传入连接保持为挂起状态。 积压值为5是很常见的。
监听函数成功返回0,错误返回-1。

Accepting Connections

一旦服务器程序创建并命名了套接字,它就可以等待通过使用accept系统调用建立与套接字的连接。

1
2
#include <sys/socket.h>
int accept(int socket, struct sockaddr *address, size_t *address_len);

当客户端程序尝试连接到由参数socket指定的套接字时,accept系统调用返回。
accept函数创建一个新的套接字来与客户端通信并返回其描述符。套接字必须先前已通过绑定调用被命名,并且监听已分配了连接队列。
调用方客户端的地址将放置在由地址指向的 sockaddr 结构中。
在调用accept之前,必须将 address_len 设置为预期的地址长度。 返回时,address_len 将被设置为呼叫客户端地址结构的实际长度。
当存在未处理的客户端连接或错误为-1时,accept函数将返回新的套接字文件描述符。

Requesting Connections

客户端程序通过调用connect在未命名套接字和服务器侦听套接字之间建立连接来连接到服务器。

1
2
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len);

参数套接字指定的套接字连接到参数地址指定的服务器套接字,该地址的长度为 address_len
如果成功,则connect返回0,并且在错误时返回-1。 如果无法立即建立连接,则连接将在未指定的超时时间内阻塞。

Closing a Socket

我们可以通过调用close终止服务器和客户端上的套接字连接。
我们应该始终关闭两端的套接字。

Host and Network Byte Ordering

端口号和地址通过套接字接口作为二进制数进行通信。
不同的计算机对整数使用不同的字节顺序。

  • 英特尔处理器将32位整数作为四个连续字节存储在内存中,顺序为1-2-3-4,其中1是最高有效字节。
  • 摩托罗拉处理器将以字节顺序4-3-2-1存储整数。

如果简单地逐字节复制用于整数的内存,则两台不同的计算机将无法在整数值上达成共识。

对于整数而言,最有意义的位是最高位。
big_endian(网络字节顺序),最有意义的字节放在一个字的最前面。
small_endian,最有意义的字节放在一个字的最后面。

为了使不同类型的计算机能够就通过网络传输的多字节整数的值达成共识,我们需要定义网络顺序。
客户端和服务器程序必须使用 netinet/in.h 中定义的函数在传输之前将其内部整数表示形式转换为网络顺序。

1
2
3
4
5
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

这些函数在本地主机格式和标准网络顺序之间转换16位和32位整数。
它们的名称是转换的缩写,例如,“host to network, long”(htonl)和“host to network, short”(htons)。
对于本机排序与网络排序相同的计算机,这些表示空操作。
始终使用转换功能以使架构不同的计算机上的客户端和服务器正确运行仍然很重要。

Network Information

如果您有权这样做,则可以将服务器添加到 /etc/services 中的已知服务列表中,该列表为端口号分配一个名称,以便客户端可以使用符号服务而不是数字。
给定计算机的名称后,您可以通过调用为您解析地址的主机数据库函数来确定 IP 地址。
主机数据库功能在接口头文件 netdb.h 中声明。

1
2
3
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
struct hostent *gethostbyname(const char *name);

这些函数返回的结构必须至少包含以下成员:

1
2
3
4
5
6
7
struct hostent {
char *h_name; /* name of the host */
char **h_aliases; /* list of aliases (nickname) */
int h_addrtype; /* address type */
int h_length; /* length in bytes of the address */
char **h_addr_list; /* list of address (network order) */
};

如果没有指定主机或地址的数据库条目,信息函数将返回一个全指针。


可通过某些服务信息功能获得有关服务和相关端口号的信息。

1
2
3
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);

proto 参数指定用于连接到服务的协议,对于 SOCK_STREAM TCP 连接为“ tcp”,对于 SOCK_DGRAM UDP 数据报为“ udp”。

结构 servent 至少包含以下成员:

1
2
3
4
5
6
struct servent {
char *s_name; /* name of the service */
char **s_aliases; /* list of aliases (alternative name) */
int s_port; /* The IP port number */
char *s_proto; /* The service type, usually "tcp" or "udp" */
}

通过调用 gethostbyname 并打印结果,可以收集有关计算机的主机数据库信息。
地址列表需要转换为适当的地址类型,并使用 inet_nota 转换从网络顺序转换为可打印字符串。

inet_ntoa有以下定义:

1
2
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in)

将网络主机的四元地址转换为一个四元格式。出错时返回-1。
您使用的另一个新函数是 gethostname

1
2
#include <unistd.h>
int gethostname(char *name, int namelength);

此函数用于将当前主机的名称写入按名称给定的字符串中。

Multiple Clients

服务器程序使用fork处理多个客户端。 在数据库应用程序中,这可能不是最佳解决方案。

  • 因为服务器程序可能很大
  • 仍然存在协调来自多个服务器副本的数据库访问的问题。

实际上,您真正需要的是单台服务器处理多个客户端的方法,而不会阻塞并等待客户端请求的到达。

select函数对数据结构 fd_set 进行操作,这些数据结构是打开文件描述符的集合。 定义了许多宏来处理这些集合:

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/time.h>
void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);

FD_ZEROfd_set 初始化为空集,FD_SETFD_CLR 集,并清除与作为 fd 传递的文件描述符相对应的集合元素。

如果 fd 引用的文件描述符是 fdset 指向的 fd_set 的元素,则 FD_ISSET 返回非零。
fd_set 结构中文件描述符的最大数量由常量 FD_SETSIZE 给出。
选择函数还可以使用超时值来防止无限期阻塞。 超时值使用结构 timeval 给出。

1
2
3
4
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
}

select系统调用具有以下原型:

1
2
3
#include <sys/types.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);

对select的调用用于测试一组文件描述符中的任何一个是否已准备好进行读取或写入,或者是否有待处理的错误条件,并且可以选择阻塞直到一个文件描述符准备就绪。
nfds 参数指定要测试的文件描述符的数量,并考虑从0到 nfds-1 的描述符。

Select 函数修改由readfdswritefdsexceptfds所指向的描述字集合。这3个参数既是传送值的参数,也是回收结果的参数。当我们调用select函数时,在这3个参数中指定感兴趣的描述符,而select函数返回时则存储结果于其中指明就绪的描述。为了检测描述字集合中那些是就绪的,要使用FD_ISSET宏调用,任何未就绪的描述字在描述字集合中对应位将清零。因此,每次调用select时,都应当重置感兴趣的描述字集合。

选择函数将返回

  • 如果 readfds 集合中的任何描述符已准备好读取,
  • 如果 writefds 集合中的任何内容准备好进行写入,
  • 如果 errorfds 中有任何错误条件。

如果以上条件均不适用,则select将在超时指定的时间间隔后返回。
如果timeout参数为空指针,并且套接字上没有任何活动,则调用将永远阻塞。
当select返回时,描述符集将被修改以指示哪些描述符已准备好读取或写入或有错误。

您应该使用 FD_ISSET 对其进行测试,以确定需要注意的描述符。
在发生超时的情况下,所有描述符集将为空。
select调用返回修改后的集合中描述符的总数。 失败时返回–1,设置 errno 来描述错误。

Datagrams

UDP提供的服务通常用于客户机需要对服务器进行简短查询并期望单个简短响应的地方。
因此,如果数据对您很重要,则需要对 UDP 客户端进行仔细编码,以检查错误并在必要时重试。
要访问 UDP 提供的服务,我们需要使用两个特定于数据报的系统调用 sendtorecvfrom

sendto 系统调用使用套接字地址和地址长度从套接字上的缓冲区发送数据报。 它的原型实质上是

1
2
int sendto(int sockfd, void *buffer, size_t len, int flags,
struct sockaddr *to, socklen_t tolen);

recvfrom 系统调用在套接字上等待来自指定地址的数据报,并将其接收到缓冲区中。 它的原型实质上是

1
2
int recvfrom(int sockfd, void *buffer, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);

如果发生错误,则 sendtorecvfrom 都将返回–1,并将适当地设置 errno