Linux C 系统编程(15)网络编程 进阶

1 套接字编程深入    

套接字编程中有许多高级技巧,使用这些技巧可以更好地操作套接字,完成网络通信的任务;掌握这些技巧,可以更好地开发高质量的网络应用程序。

1.1 bind函数的重要作用

服务器端程序与客户端程序的显著特点就是客户端不需要bind监听函数。bind函数是将套接字绑定一个IP地址和端口号;如果没有使用bind函数绑定地址和端口,则在调用listen和connect函数时内核会自动为套接字绑定,因此,理论上调用bind函数是可以省略的;但事实上listen和connect绑定的形式是不一样的,如下所示:

  1. listen绑定:listen函数中没有地址结构这个参数,因此只能由系统设置IP地址 和端口号。
  2. connect绑定:使用一个设置好的结构(sockaddr_in)作为参数,结构中指定了要绑定的服务器。

服务器端程序不关心客户端的IP地址,内核会为其绑定为任意值(INADDR_ANY),端口号也会由内核指派一个可用的端口。(由于是临时指派,所以会导致每次执行服务器程序时使用的端口不一样,这就要求客户端程序每次都要更改端口号,而这并不现实)
由此可知,bind函数对于服务器而言很重要。    

1.2 并发服务器

前面面向连接的服务器有一个弊端,那就是一次只能处理一个客户端请求,但是如果有一个客户端程序占用服务器不放,则其他的客户机将被“饿死”而不能工作;因此,在现实当中一个面向连接的服务器不会使用循环框架,取而代之的是使用对进程处理的方式来处理多个请求,即并发服务器,并发服务器执行流程(伪代码)如下:

//地址结构初始化;
fd=socket();
bind(fd,...);
listen(fd,...);
while(1){
     accept_fd=accept(fd,...);
     if(fork(...)==0){
          //与客户端交互,处理来自客户端的请求;
          close(accept_fd);
     }
     close(accept_fd);
}
close(fd);
close函数失败的处理。

并发服务器解决了循环服务器客户机独占服务器的情况,但也要注意两个新问题:

  1. 创建子进程非常消耗资源,为提高效率必须使用优秀的算法。
  2. 子进程结束后,要注意对资源进行回收,这是一个可以使系统崩溃的潜在问题。    

1.3 UDP协议的connect函数应用

connect函数也可以用于UDP连接,虽然UDP属于面向无连接通信协议,但如果通信时数据报的目的地址总是固定的,那么每次都这样操作有些不必要,这时使用connect来就爱你里一个连接反而使通信变得更有效率。此时客户端的执行流程与面向连接的客户机一致;了不同指出在于一个是面向连接,一个不面向连接。流程(伪代码)如下:

//地址结构初始化;
fd=socket(“UDP”);
connect(fd,...);
//与服务器交互,向服务器发出具体消息/接受来自服务器的消息;
close(fd);

当数据通信量很大时,这种发方法的效率将高于传统的无连接通信方法。


2 多路选择I/O

多路I/O是另一种处理I/O的方法,比传统的I/O更有效率,是一种充分利用时间的典型,在网络应用中很常用。

2.1 多路选择I/O概念

多路选择I/O主要是为了防止I/O阻塞,避免进程陷入僵死状态。这种方法的思想就是构造一张需要读取数据的设备表(通常是文件描述),调用一个函数轮询这个表中的设备,知道有一个设备可以读写,该函数才返回。多路I/O模型如图所示:
 

多路选择I/O需要使用两个系统调用:

  1. 负责检查并返回可用设备的文件描述符。
  2. 负责对该文件描述符进行读写。

2.2 实现多路选择I/O

linux下使用select函数实现所路选择I/O,函数原型如下:

#include <sys/select.h>     /* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
//fd_set这种数据类型本质上是一个位向量,即一个无符号整数;每一位代表一个状态,为1表示被设置,为0表示没有被设置,

//linux环境下提供专门对这种向量进行操作的函数,如下所示:
void FD_CLR(int fd, fd_set *set);          //清除向量指定的位。
int  FD_ISSET(int fd, fd_set *set);     //测试向量指定的位是否被设置,fd表示需要测试的位。
void FD_SET(int fd, fd_set *set);          //设置向量指定的位。
void FD_ZERO(fd_set *set);               //清空位向量所有的位。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
部分参数说明:
参数timeout如果为NULL则表示一直等待设备就绪,是一种死等设备的方法。
如果readfds、writefds、exceptfds都被置为NULL,即表示对三种状态都不关心,则此时select函数成为一个精度为微秒的定时器;select函数将一直查询各个设备,直到时间耗尽为止。  
函数返回值:正常返回准备好的设备数;如果为0,表示没有设备准备好;为-1表示出错。

详细见linux函数参考手册。注意:默认情况下,一个进程最多有1024个文件描述符。    

2.3 屏蔽信号的多路选择I/O

pselect函数与select函数的区别:

  1. pselect最后一个参数是一个信号屏蔽的选项,即拥有信号屏蔽的功能;其中不可以屏蔽的信号有SIGKILL和SIGSTOP。(防止恶意程序攻击计算机)
  2. pselect中的时间参数用的结构是timespec,timespec结构所能表示的最小精度是纳秒;如果将其作为定时器使用,将是linux中最精确的定时器。

linux下使用pselect函数来实现屏蔽信号的多路选择,函数原型如下:

int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);

详细见linux函数参考手册

2.4 多路选择I/O的服务器端流程

//地址结构初始化;
fd=socket();
bind(fd,...);
listen(fd,...);
//FD系列函数的操作,初始化
     FD_ZERO();
     FD_SET();
     FD_ISET();
     FD_CLR();
while(1){
     numner_fd=select();
     accept_fd=accept(fd,...);
     与客户端交互,处理来自客户端的请求;
     close(fd);
     if(--number_fd<=0)
          break;
}
close(fd);
close函数失败的处理。

2.5 轮询I/O

select函数在使用中是不支持STREAMS的,所以系统在select的基础上又添加了poll函数,poll函数支持各种类型文件描述符做I/O多路转换;但是与select不同的是,poll函数独立为每个要监控的文件描述符建立一个操作结构体pollfd,结构中描述了监控的行为和目标文件描述符发生的事件,结构体定义如下:

# include < sys/ poll. h>
struct pollfd {
     int fd;         /* 文件描述符 */
     short events;         /* 等待的事件;用户所关心的操作 */
     short revents;       /* 实际发生了的事件;文件描述符上已经发生的事件 */
} ;

events和revents成员设定以下标志之一/多个标志的组合,如表所示:

注意:成员events不可以被设置为异常标志;成员revents为调用poll函数时返回的数据,所以在调用前不需要设置。linux下poll函数原型如下:    

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

详细见linux函数参考手册


3 非网络通信套接字

非网络通信套接字主要用于本机内的进程通信;由于本机内进程的IP地址相同,因此只需要进程号来确定通信的双方。

3.1 非命名UNIX域套接字

linux下使用socketpair函数创造一对未命名的、相互连接的UNIX域套接字,函数原型如下:

#include <sys/types.h>         
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);

详细见linux函数参考手册。socketpair函数创建一对未命名的UNIX域套接字,由于没有命名,所以其他进程不能使用该套接字进行通信,也就是说,只有保存了未命名的UNIX域套接字的文件描述符的进程才可以使用它。这一点有点类似于管道,父进程创建一个子进程,二者都保存管道两端的文件描述符,之后各自关闭管道的一端开始通信。

3.2 命名UNIX域套接字

未命名UNIX域套接字限制了通信的范围,不够灵活;而命名套接字可以解决这个问题。与网络通信套接字一样,UNIX域套接字也需要绑定地址,不过由于二者所使用的域不一样,因此其地址族也不相同。在Linux系统中,UNIX域套接字使用sockaddr_un结构存储地址,其结构原型如下:

#include <sys/un.h>
struct sockaddr_un{
    sa_family_t sun_family;          //表示地址使用的族,即AF_UNIX
    char sun_path[108];              //套接字文件的路径名
};

该地址绑定到一个UNIX域套接字时系统会创建一个文件,其路径名为sun_path中所表示的路径名,类型为S_IFSOCK。这个文件不能打开,需要进行通信的进程都绑定到这个文件上去,该文件的作用类似一个信息的中转站。在调用bind函数进行地址绑定时,需要将字符数组sun_path的大小作为参数传递给内核,通常的写法是:

int sfd;
struct sockaddr_un un;
bind(sfd, (struct sockaddr *)*un,  sizeof(struct scokaddr_un) - sizeof(sa_family_t));

详细见linux函数参考手册。其中,sun_path的大小为sizeof(struct scokaddr_un) - sizeof(sa_family_t),这样做是为了提高代码的移植性。注意:

  1. 在客户端创建本地套接字时需调用bind与相应的路径相绑定,这是必须的一步,不同于网络套接字,其实网络套接字编程也是需要bind,只不过内核自动隐式进行绑定;
  2. 在创建套接字绑定之前应先unlink预绑定的路径套接字文件,否则bind将出错;
  3. 在将套接字与相应的路径绑定之后,若不再需要通过该有名路径与其它套接字相连,可以unlink该名字路径。

由此可知,进程使用套接字文件的磁盘文件进行通信,但每次进行地址绑定时不希望该文件的目录项存在。通常服务器的进程套接字磁盘文件会长时间存在,其目录项也会不断地被删除和创建。每一次客户端连接,都会在绑定服务器端地址时创建一次,接着就删除该文件,负责下一个客户端的连接将无法绑定服务器的地址。客户端进程的套接字文件在通信结束后就删除了,每一个客户端进程都必须拥有一个套接字文件;为保证该文件在系统的唯一性,通常的做法是使用进程的ID作为文件名,这样就可以保证文件名不会冲突。

3.3 UNIX域套接字的服务器端与客户端流程以及注意事项

UNIX域套接字的使用方法和网络通信套接字的使用方法一样,也分为服务器进程和客户端进程。服务器的执行流程和客户端的执行流程与网络通信套接字的执行流程一致。

发布了289 篇原创文章 · 获赞 47 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/vviccc/article/details/105175132
今日推荐