现在的位置: 首页 > 综合 > 正文

Linux程序设计——套接字

2013年10月09日 ⁄ 综合 ⁄ 共 5895字 ⁄ 字号 评论关闭

进程间通信的机制包括信号量、共享内存、管道和消息队列等,但是这些机制只能实现在一台计算的进程间通信。本文将介绍另外一种进程间通信的机制——套接字,可以实现计算机网络中的通信。

1、套接字

套接字是一种通信机制,通过使用这种通信机制,客户/服务器系统的开发既可以在本地单机上进行,也可以跨网络进行。套接字明确地将客户和服务器区分开来,可以实现将多个客户连接到一个服务器。

2、套接字连接

套接字维持连接:首先,服务器应用程序通过系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,不能与其他进程共享。然后服务器进程通过系统调用bind给套接字起个名字。命名后服务器进程就开始等待客户连接到这个命名套接字。最后,服务器通过系统调用accept来接受客户的连接。而对于客户来说,首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。

3、套接字属性

套接字的特性由3个属性决定:域(domain)、类型(type)和协议(protocol)。

1)套按字的域

域指定套接字通信中使用的网络介质。常用的是AF_INET指的是Internet网络,底层协议是网际协议(IP),地址是IP地址。AF_UNIX是UNIX文件系统域,底层协议是文件输入/输出,地址是文件名。

2)套接字类型:流(stream)是数据报(datagram)

流套接字提供的是一个有序、可靠、双向字节流的连接。因此发送的数据可以确保不会丢失、复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息将分片、传输、再重组。流套接字由类型SOCK_STREAM指定,在AF_INET域中通过TCP/IP连接实现,它同时也是AF_UNIX域中常用的套接字类型。

数据报套接字由类型SOCK_DGRAM指定。它不建立和维持一个连接,对可以发送的数据报的长度有限制。在传输过程中,数据报可能会丢失、复制或乱序到达。数据报套接字在AF_INET域中是通过UDP/IP连接实现的,提供一种无序的不可靠服务。但从资源的角度来看,相对开销比较小,因为不需要维持网络连接,而且因为无需花费时间建立连接,所以速度也很快。

3)套接字协议

4、套接字操作

1)创建套接字

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。

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

domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。最常用的套接字域是AF_UNIXt A_INET。type的取值包括SOCK_STREAM和SOCK_DGRAM。通信所使用的协议一般是由套接字类型和套接字域来决定的,通常不需要选择。protocol参数调用为0表示使用默认协议。

建立套接字后,可以用read和write系统调用利用返回的描述符在套接字上发送和接收数据。

2)套接字地址

每个套接字域都有自己的地址格式。对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述。

#include <sys/un.h>
struct sockaddr_un
{
    sa_family_t sun_family;  /* AF_UNIX */
    char        sun_path[];  /* pathname */
};

在AF_INET域中,套接字地址由sockaddr_in来指定,该结构定义在头文件netinet/in.h中

#incldue <netinet/in.h>
struct sockaddr_in
{
    short int      sin_family;  /* AF_INET */
    unsigned short int sin_port;  /* Port Number */
    struct in_addr sin_addr;  /* Internet address */
};

通过套接字接口传递的端口(sin_port)和地址(sin_addr)都是二进制数字。而不同计算机使用不同的字节序来表示整数,因此客户和服务器程序必须在传输之前,将它们的内部整数表示方式转换为网络字节序。

#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位整数在主机字节序和标准网络字节序之间进行转换。IP地址in_addr的定义为

struct in_addr
{
    unsigned long int s_addr;
};

IP地址中的4个字节组成一个32位的值。一个AF_INET套接字由它的域、IP地址和端口号来完全确定。
3)命名套接字

要想让通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名,AF_UNIX套接字关联到一个文件系统的路径名,AF_INET套接字关联到一个IP端口号。

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

bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字。地址结构长度由参数address_len指定。

4)创建套接字队列

为了能够在套接字上接受进行的连接,服务器程序必须创建一个队列来保存未处理的请求。

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

backlog参数指定listen系统调用创建的队列的长度,在套接字队列中,等待处理的进入的连接的个数最多不超过这个数字。超过的连接将被拒绝,导致客户连接请求失败。

5)接受连接

服务器程序创建并命名了套接字之后,可以通过accept系统调用来等待客户建立对该套接字的连接。

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

accept系统调用只有当客户程序试图连接到由socket参数指定的套接字上时才返回。此时,accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符。address参数指向连接客户的地址。address_len参数指定客户结构的长度。如果客户地址长度超过这个值,它将被截断。所以在调用accept之前,address_len必须被设置为预期的地址长度。

如果套接字队列中没有未处理的连接,accept将阻塞直到有客户建立连接为止。如果想改变这一行为,可以设置套接字文件描述符指定O_NONBLOCK标志。

int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, O_NONBLOCK | flags);

6)请求连接

客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。

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

参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定。

7)关闭套接字

通过调用close函数终止服务器和客户上的套接字连接。

5、网络信息

对于一台主机,如何通过已经地址信息或主机名获取其他网络信息,是非常重要的。在Linux系统中,头文件netdb.h定义了与之相关的一些函数接口。

#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
struct hostent *gethostbyname(const char *name);
int gethostname(char *name, int namelength);   /* 将当前主机的名字写入name指向的字符串中 */

这些函数返回hostent结构体,结构体定义如下

struct hostent
{
    char *h_name;        /* name of the host */
    char **h_aliases;    /* list of aliases(nicknames) */
    int h_addrtype;      /* address type */
    int h_length;        /* length in bytes of the address */
    char **h_addr_list;  /* list of address(network order) */
};

同样,与服务及其关联端口号有关的信息也可以通过一些服务信息函数来获取。

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

proto参数指定用于连接该服务的协议,它的两个取值是tcp和udp。结构servent的定义如下

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

获得某台主机的信息后,要把返回的地址列表转换为正确的地址类型,并用函数inet_ntoa将它们从网络字节序转换成可打印的字符串。

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in); /* 将一个因特网主机地址转换成一上点分四元组格式的字符串 */

6、因特网守护进程(xinetd/inetd)

Linux系统通常是以超级服务器的方式来提供多项网络服务。超级服务器程序也就是因特网守护进程xinetd或inetd同时监听多个端口地址上的连接。当有客户连接到某项服务时,守护进程就运行相应的服务器。这使得针对各项网络服务的服务器不需要一直运行着,可以在需要时启动。配置文件通常是/etc/xinetd.conf和/etc/xinetd.d目录中的文件。Ubuntu下图形化工具常用的是Bootup-Manager。

7、select系统调用

select系统调用允许程序同时在多个底层文件描述符上等待输入的到达。select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合。

#include <sys/types.h>
#include <sys/time.h>
void FD_ZERO(fd_set *fdset);          /* 初始化集合 */
void FD_CLR(int fd, fd_set *fdset);   /* 在集合中设置由参数fd传递的文件描述符 */
void FD_SET(int fd, fd_set *fdset);   /* 在集合中清除由参数fd传递的文件描述符 */
int FD_ISSET(int fd, fd_set *fdset);  /* 判断由fd指定的文件描述符是否在fset集合中 */

select函数可以用一个超时值来防止无限期的阻塞。这个超时值由timeval结构给出。

struct timeval
{
    time_t tv_sec;   /* seconds */
    long   tv_usec;  /* microseconds */
};

select系统调用定义如下

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

select调用用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态,它将阻塞以等待某个文件描述符进入上述这些状态。参数nfds指定需要测试的文件描述符的数目,测试描述符的范围从0到nfds-1。当select返回时,描述符集合将被修改以指示哪些描述符正处于可读、可写或有错误状态。如果select是因为超时而返回的话,所有描述符集合都将被清空。select调用返回状态发生变化的描述符总数,失败时返回-1并设置errno来描述错误。

8、数据报

当客户需要发送一个短小的查询请求给服务器,并且期望收到一个短小的响应时,一般使用由UDP提供的服务。因为UDP提供的是不可靠的服务,所以数据报或响应可能会丢失,,因此需要检查错误并在必要时重传。UDP提供使用两个专用的系统调用sendto和recvfrom来代替原来使用在套接字上的read和write调用。

int sento(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);
int recvfrom(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

sento系统调用从buffer缓存区中给使用指定套接字地址的目标服务器发送一个数据报。recvfrom系统调用在套接字上等待从特定地址到来的数据报,并将它放入buffer缓存区。当调用出现错误时,sento和recvfrom都返回-1并设置errno。

总结

本文主要介绍进程中通信的方法:套接字。通过套接字可以实现跨网络运行的分布式客户/服务器程序。并介绍了select系统调用,它允许一个程序同时在多个打开的文件描述符和套接字上等待输入和输出活动。

抱歉!评论已关闭.