LINUX网络编程的读书笔记

来源:互联网 发布:迷糊系统 相似矩阵 编辑:程序博客网 时间:2024/06/06 07:04

转自:http://wenku.baidu.com/view/f2bb448884868762caaed5c6.html

LINUX网络编程的读书笔记

笔记是该书内容的精简,适当之处加上我个人的观点。联系:zhangyv163@tom.com

笔记整理:ZhangYv     日期:2005-1-15

 

书名:Linux网络编程   作者:林宇 郭凌云     出版社:人民邮电

难度:入门到进阶     

 

第一章 文件系统和进程系统

11文件系统的总体结构

       从文件系统的实现角度来看,按层次可以分成应用程序、系统调用、文件子系统、高速缓冲、设备驱动和具体的存储设备等几个层次,如下图:

      

应用程序

系统调用接口

文件子系统

硬件存储设备

设备驱动程序

高速缓存

字符设备

块设备

UNIX系统中,程序不管核心按照什么样的格式来组织文件,只是把文件看作一个无格式的字节流来看待。对文件的存取语法是由系统定义的,数据的语义是由程序加上去的。

应用进程通过系统调用来访问文件系统,分配给应用程序一个标准的通用接口, 便于屏蔽不同文件系统的差异。文件系统不能直接访问硬件设备,通过调用设备驱动进程来操作具体设备。对高速设备的访问,通常通过高速缓冲机制来提高设备和内存的数据交换。设备驱动进程用来屏蔽不同物理设备的操作差异。

 

文件系统的总体结构是:引导块、超级块、索引节点表,数据区。

·引导块在文件系统的最前面,它和操作系统引导有关。有且只有一个引导块有效。

·超级块也叫管理块,存放文件系统的管理信息,如文件系统大小、空闲块大小、空闲块链表节点头等信息。

·索引节点表,每个文件都对应着一个索引节点,里面反正用户的存取权限、信息等。通过路径访问文件,内核把文件路径经过转换映射到索引节点表中对应节点去。

·数据区。文件系统实际存放数据的磁盘空间。

·空闲数据块表。超级块中空间很小,所以把空闲数据块的信息写在数据区中。

 

VFS(Virtual Filesystem Switch)

LINUX通过虚拟文件系统转换来实现多文件系统的支持。LINUX把对文件操作的系统调用转为对不通过文件系统操作的子程序调用,这些子程序都针对具体文件系统而编写。虚拟文件系统不是真正的文件系统,而是一种映射机制来屏蔽下层的差异为上层提供方便。

 

 

12 文件结构和目录结构

LINUX中的每个文件都对应虚拟文件系统的一个索引节点,里面存放有直接或多级指针能够记录文件的数据,这样设计是为了存取大文件。

 

目录也能抽象成文件,也通过索引节点表来描述,并且把目录表中的目录项存放在数据区中。目录表的基本构成单位是目录项,有“文件名-索引节点号”构成。文件节点索引表中并不包含文件名这个信息,文件名被填写在目录文件中。

 

·硬连接和符号(软)连接的区别:

硬连接能实现的功能符号连接都能实现。硬连接只能用在文件(非目录)和同一个文件系统,但是符号连接适用在目录,也适用在不同的文件系统间。但是符号连接比硬连接更消耗内核资源,因为符号连接的转换规则是在内核中实现的,而硬连接则直接指向索引节点。

硬连接是文件名和索引节点的对应关系;符号连接是指向文件的路径

 

·文件系统相关编程:

从系统的实现角度来看,文件内在表示是唯一确定的索引节点。如果从编程角度来看,文件可以通过文件描述符和文件指针来表示。UNIX I/O库中有open,write,read,close,ioctl等系统调用来操作文件描述符。

C库函数中,有fopen, fprintf, fread, fwrite, fclose等文件操作函数对文件指针进行处理,它们是对系统调用的再次封装。

 

从系统角度来说:文件句柄就是文件的一种标志,是文件描述符表中的索引号。进程的标志输入、输出和错误输出的文件描述符分别是012unistd.h中将它们定义为STDIN_FILENO, STDOUT_FILENOSTDERR_FILENO

C函数库角度来说:文件句柄是一个指向文件结构的指针。。进程的标志输入、输出和错误输出在stdio.h中被定义为stdin, stdout, stderr。可以使用系统调用fileno()将一个文件指针转为文件描述符。

 

 

1.3 进程系统

·程序并行执行中的问题:

静态程序的概念不能很好描述并行环境下的规律,因此引入的进程的概念。单道程序设计中,环境是封闭的,资源总被独占;而在并行环境中由于封闭性和资源的独占性被破坏,这将导致很多问题。

 

·进程和程序的区别:

程序是指令和数据的集合,是一个静态文本,存放在一个普通的文件中,该文件在索引节点表中的文件标志为“可执行”。

进程是程序在一个包括指令段、系统和用户数据的环境中,为了完成预定的任务而运行一次的过程。进程被撤销后就不再存在,而程序的文本依然留在系统中。

 

·进程的物理表示:

为了描述动态变化的进程,我们把进程静态的分为3个部分:程序部分、数据部分、进程控制块——统一称为进程映像。

 

进程的程序部分可以被多个程序所共享,共享代码段应该被编写成纯代码puer code,即该程序段的功能不随着调用的程序不同而存在差异。

程序段被执行的数据区和工作单元,当执行的不是共享代码段时,数据的一部分就被放入数据空间。

每个进程都有一个进程控制块PCB,用来跟踪并记录动态变化的进程执行和调动信息的数据结构,集中体现了进程的特征、状态和其他进程间的关系等。

 

·进程的虚空间:

操作系统的虚空间可以分成“进程虚空间”和“系统虚空间”。

 

可执行程序的指令和数据对应着进程虚空间的地址,由操作系统把进程虚空间地址映射到物理内存上。这种映射是通过硬件寄存器和系统页表共同实现的。

 

·用户态和核心态:

用户态和核心态实际上是CPU工作的两种不同模式。所有内核对外提供的功能都是按系统调用的形式。进程进行一次系统调用,CPU将在用户态与核心态间切换一次,系统调用工作在核心栈,而普通用户调用将使用用户栈。

 

·进程上下文:

进程在生命期的所有状态都可以通过进程上下文来描述。通常包括三个内容:

 1 用户级上下文:包括代码段、数据段、用户段和共享内存段。

 2 寄存器上下文:进程运行时各寄存器的内容

 3 系统级上下文:进程控制块、进程使用的页表和核心栈

 

·进程转换

stop

 

continue

 

wakeup

 

continue

 

stop

 

wakeup

 

stop

 

switch

 

switch

 

fork

 

fork

 

系统调用或中断

 

系统调用或中断返回

 

wait

 

sleep

 

exit

 

用户态运行

核心态运行

就绪

暂停

睡眠

初始空间

僵尸状态

暂停+睡眠

 

·进程调度:

核心将在几种情况下调用调度管理器:当前进程被放入等待队列或者系统调用结束时,以及从核心态返回到用户态时。

(1)    LINUX支持两类不同进程:普通与实时进程,不同之处体现在优先级和调度策略上。

(2)    如果一个实时进程处于可执行状态,它总在任何普通进程前执行。

(3)    实时进程采用两种调用策略:时间片轮转和先进先出。

(4)    普通进程采用Round Robin策略。

(5)    priority进程优先级、rt_priority实时进程优先级、counter进程运行运行时间

 

·对fork的理解:

fork之后父子进程的tast_struc除了进程号,其他的数据都一样。利用虚空间技术,共享代码段(引用计数加1),复制数据段。fork快完成的某阶段子进程被建立并保存上下文进入就绪队列等待调度,fork完毕之后父进程上下文被保存,返回子进程的进程标识符。注意:子进程的fork调用返回是0,父进程fork调用返回是子进程的进程号。然后父子进程从fork的调用点开始分别继续运行。

父进程退出前需要使用wait()waipid()等待子进程执行完毕和清除僵尸进程释放资源。

 

第二章  进程间通信和同步

前言:在linux/unix中支持多种进程间通信(IPC)的方式,主要包括:信号、信号量、消息队列和共享内存,管道(包括无名管道和FIFO)也是进程间通信的方式。

 

·2,2信号的捕获和处理:

#inlucde <signal.h>  //参见POSIX.1中定义

相关函数:

sigaction(int signo, const struct sigaction *act, struct sigaction *oact); //设置信号处理器

struct sigaction{

       void (*sa_handler)();

       sigset_t sa_mask;

       int sa_flags;

};

(1) 信号处理器函数指针 (2)进程屏蔽的信号集合  3)信号处理器的标志(查阅手册)

 

int sigemptyset(sigset-t *set);              //信号集合清空

int sigfillset(sigset_t *set);            //设置包含所有信号的全集

int sigaddset(sigset_t *set, int signo);    //把一个信号加入信号集合

int sigdelset(sigset_t *set, int signo);     //把一个信号从集合里删除

int sigismember(const sigset_t *set, int signo);    //判断信号是否包含在给定集合中

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);     //设置进程中断屏蔽码

how = [SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK], *oset对设置前屏蔽码做备份

 

使用信号处理器基本方法:

1.       编写信号处理函数handler_sigproc();

//信号处理函数执行完毕的最后,记得要清堵塞的信号

//sigaddset(&blockmask, SIGINT);     //信号处理器缺省堵塞的信号

//sigaddset(&blockmask, SIGTERM);  //信号处理器处理的信号

//sigprocmask(SIG_BLOCK, &blockmask, NULL); //清堵塞信号

2.       设置信号处理器struct action act;

act.sa_handler = handler_sigproc;

sigemptyset(act.sa_mask);

sigaddset(&act.sa_mask, SIGTERM); //信号处理器执行期间堵塞相应的信号

sigaction(SIGTERM, &act, NULL);//(kill产生)终止信号加入act信号处理器

 

快系统调用、慢系统调用都可能被信号打断,POSIX.1把被中断的系统调用返回-1errno设置为EINTER,只要不是“原子操作”都可能被打断,注意对这类问题的容错处理:

ret = read(fd, buf, 255); 

if (ret == -1 && errno == EINTER)  //如果 (系统调用是由中断引起的执行失败) 则……

 

·2.3 信号量

有名信号量是全局,只要知道它的名字就可以使用它;

无名信号量是局部,只能通过继承才能使用它;

 

相关函数:

头文件:<sys/types.h>, <sys/ipc.h>, <sys/sem.h>

int semget(key_t key, int nsems, int semflg);              //创建或取得一个信号量组

int semctl(int sem_id, int semnum, int cmd);      //信号量控制函数(取值/删除/设置等)

int semop(int semid, struct sembuf *sops, int nsops); //信号量操作函数

(1)    信号量组ID 2)进行怎样操作 3)操作次数

struct sembuf{

       short sem_num;     //对信号量组第sem_num个进行操作

       short sem_op;        //对信号量sem_value执行 -1P操作,+1V操作

       short sem_flg;        //通常取0,如果使用SEM_UNDO退出进程后,信号量值变为0

};

使用信号量基本流程:
1. sem_id = semget(SEM_KEY,0,0); //SEM_KEY
自定义,要确保唯一性

2.  if (sem_id != -1) //如果 信号量组不存在

              sem_id = semget(SEM_KEY, SEM_NUM, IPC_CREAT|IPC_EXCL|0666)

        ...//创建资源为SEM_NUM个的一个信号量组,权限为0666(可读写)

else  初始化信号量组的信号量资源个数

3实现PV操作函数:

void P(int sem_num, int sem_id)//对信号量组sem_id的第sem_num个信号量操作

{

       struct sembuf sem[1];

       sem[0].sem_num=sem_num;  sem[0].sem_op = -1; sem[0].sem_flg = 0;

       if (semop(sem_id, sem, 1) == -1) //... 执行一次P操作,V操作类似

}

4 semctl(sem_id, sem_index, IPC_RMID); //手动删除信号量组

//注意“信号量组”和“信号量值”的区别!

 

·2.4  消息队列

#include <sys/msg.h>

int msgget(key_t key, int msgflg));  //创建或取得消息队列的ID,和信号量组类似

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

int msgsnd(int msqid, void *msgp, size_t msgsz, int msgflg));

int msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg)); //接收消息

//msgtyp=0:返回第一个消息 >0:返回第一个值=msgtyp的消息 <0:返回第一个值<=-msgtyp

 

 

消息队列使用基本原理:

子进程child发送首次登记的标志FLAG(msgtyp>0)child进程号到服务器进程server注册,在server段使用msgrcv(Q_MSG_KEY, &recv_buf, sizeof(Message)-sizeof(long), FLAG, 0)接收,Message正文不包括消息头的标志。然后server端发送server进程号,接收消息标志为子进程号的Message到子进程表示接收到先前消息。

 

―――――――――――――――――――――――――――――――――――――――

Message send_msg;              //首次登记并提交本进程的ID

send_msg.m_type = FLAG;

send_msg.process_id = getpid();

send_len = sizeof(long)+sizeof(int);

ret = msgsnd(msq_key, &send_msg, send_len, 0);

Message recv_msg, reply_msg;

//接收标志为FLAG的消息

ret = msgrcv(msq_key, &recv_msg, sizeof(Message)-sizeof(long), getpid(),FLAG,0);

reply_msg.m_type = recv_msg.process_id;   //向子进程发送反馈消息

reply_msg.process_id = getpid()           //告诉子进程服务端server进程号,准备建立交互

 

 

消息队列通过消息标志(即进程号)进行通信,如果客户/服务端进程有任何一方退出,则可能会出现消息丢失。即把退出一方的进程号作为标志的消息不会被任何进程接收,因为其他的进程号和消息标志不匹配。

 

建立连接开始数据通信

回复反馈信息

发送注册信息

Server

Client

消息队列通信

·2.5 共享内存

共享内存就是多个进程共享一端物理内存空间,通过把一段物理内存地址映射不同的虚空间来实现,而消息队列是把数据从应用缓冲区到核心缓冲区往返复制。因此共享内存的通信使用效率比消息队列高,但存在复杂的同步互斥关系。

 

函数:

int shmget((key_t key, int size, int shmflg));                      //创建或取得一块共享内存

int shmctl((int shmid, int cmd, struct shmid_ds *buf));             //共享内存操作

void *shmat((int shmid, const void *shmaddr, int shmflg));       //获取共享内存的指针

int shmdt((const void *shmaddr));                                   //将共享内存块从进程中分离/删除

 

 

·小结:

消息队列可以进行多路复用,进程间同步不需要复杂的同步互斥。数据以流的方式传递,

各消息都是独立且可以区分的。信息的具体语义需要收发两端的进程自己去定义和解释。消

息队列的缺点是需要进行两次数据复制,从用户空间到核心,在从核心到用户空间。

共享内存不需要多次拷贝,在数据量大的进程间同步中,用共享内存方式可以提高效率,

但是会需要复杂的进程间同步互斥控制。

 

 

第三章  TCP/IP协议

·3.1 OSI参考模型、协议和服务

物理层实现在通信信道的01比特传输;

数据链路层加强了比特传输功能,将01比特组织成数据帧实现可靠传输;

网络层主要实现路由选择,确定端到端的传输路径;

传输层实现点对点的无差错数据传输;

会话层主要实现用户会话关系和同步的管理;

表示层消除信息的语法和语义的差别;

应用层面向不同需求,实现不同功能。

 

·3.2 TCPUDP的比较

TCP实现了面向连接的、端对端的可靠流传输。TCP为其可靠性做的最重要的工作有:确认和超时重发、以及流量控制等。适合在传输数据可靠性较高的应用。

UDP建立在IP协议上,利用IP包提供一种无连接的高效服务。但它不考虑数据包的正确和可靠性,需要应用程序自己来处理。适合在实时、数据量较小或网络通信可靠时的应用。

 

·3.3 传输层端口

       传输层和网络层最重要的区别是提供了“进程到进程的”通信能力,而网络层只能将IP包寻找到主机。实现进程间通信,除了主机地址还需要进程标志。

       TCP, UDP提出了协议端口的概念,此端口是软件端口不同于硬件端口。TCP/IP实现中对端口操作被设计成如同一般文件的操作。TCPUDP的端口是完全独立的,即使在同一个进程里使用如9999/TCP9999/UDP的端口号,它们是不会起冲突的。

       端口分配有两种:1 全局分配,即集中控制方式,由权威机构根据需要统一分配;2 本地分配,进程需要传输服务时向系统动态申请,操作系统根据当前系统端口使用情况返回本地唯一的端口号。由于端口的唯一性,也可以来标示一个进程。

       TCP/IP把端口分为两部分:1 少量的保留端口 2 自由端口,由进程进行通信前申请。

 

·3.4 域名系统和名字服务器

       域名解析就是实现IP地址和主机名字间一一对应关系。正向解析是从域名映射到IP地址,反向解析是从IP地址得到域名。在TCP/IP中,名字和地址间转换是由一组相互独立和协作的服务器软件来完成,即名字服务器。

 

·3.5  TCP协议

1. TCP的确认机制

       TCP传输数据是以字节流方式,流中的数据是一个字节构成的序列,序列结构由应用程序解释,TCP的基本传输单元是TCP数据段。当接收端收到数据后,如果数据正确TCP将发送确认信息给发送端,确认值是下一个字节的序列ACK。表明发送端的ACK之前的序列都已被正确接收。

 

2. TCP的超时重传机制

TCP在发送一个数据包后,数据信息还保留在缓冲区中,直到接收端发送确认信息后才删除它们。如果一段时间后没有收到接收端确认,那么发送端将重发该数据包然后等待再次确认。如果超时重发到达一定次数,那么发送方认为对端不可到达,断开TCP连接。

       TCP采用一种自适应的确定定时时长算法。定义RTT(round trip time)为发出数据包到数据包确认之间的时间长度。TCP检查每一连接的性能,根据变化重新计算RTT值:

Timeout = β× RTT

RTT = α×old_RTT + (1-α) × new_RTT_Sample

其中α决定RTT对时延变化反映的速度,如果α接近1,则时间变化不影响RTT值,如果接近0那么RTT将随时延快速变化。

 

       TCP数据段数据结构,略;

 

3. TCP的滑动窗口协议

       TCP通过滑动窗口协议实现拥挤控制,即发送方最多只能发送控制窗口大小的数据,当有接收方发送来的数据确认,发送才继续进行。控制窗口的大小由两个因素决定:一个是发送方自身的拥塞窗口控制;而是控制窗口大小是发送和接收两方中的最小值。

 

4. TCP的“慢启动”策略

TCP发现丢失数据则认为网络拥挤,拥塞窗口大小就减半。当TCP认为拥塞结束,就使用“慢启动”策略:每收到一个数据包拥塞窗口加1,直到窗口数达到上次发生拥塞时窗口一半时候,这是只有发送出去的所有数据包都得到回应,拥挤窗口才加1

 

5. 小结

TCP通过确认和超时重传机制保证数据包的可靠性;利用滑动窗口协议和“慢启动”策略进行流量控制。UDP协议没有上述功能,所以实时性好,但可靠性差。

 

·3.6 TCP的状态转移过程

       一个TCP连接在它的生命周期中,将经历一系列状态:LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT, CLOSED

 

       TCP连接建立过程,由A端的TCP发送请求,对端BTCP回应:

(1)    A --> B SYN my sequence number is X       <SEQ=X><CTL=SYN>

(2)    A <-- B ACK your sequence number is X     

(3)    A --> B SYN my sequence number is Y        <SEQ=Y><ACK=X><CTL=SYN,ACK>

(4)    A <-- B ACK your sequence number is Y       <SEQ=X+1><ACK=Y+1><CTL=SYN,ACK>

AB发送自己的同步SYN信息给对方后,在SYN中包括本端初始的数据序列号,并且需要接收对方对自身发从的SYNACK确认。这个过程称为“三次握手”,共发送了3个数据包传递了4个信息。

 

       TCP连接关闭前设置了TIME-WAIT状态,TCP将在等待2MSL(Maximum Segment Lifetime)时间后进入CLOSED状态。其中MSL是数据段在网络中最大生存时间。对端TCP在发送FIN数据端进行关闭确认时候,由于IP协议不可靠传输,可能主动方发送的确认数据包还未到达对端,对端就开始进行超时重发,如果这时主动端关闭TCP连接,那么TCP协议会认为发生网络连接错误,将发送RST旧连接数据段。因此主动端关闭TCP前等待2MSL时间,将确保发送和接收端的数据包在网络中消失。由A端主动发起连接断开状态图:

TCP A                                           TCP B

   ESTABLISHED                                    ESTABLISHED

     (主动关闭)

   FIN-WAIT-1 --><SEQ=X><ACK=Y><CTL=FIN,ACK>  -->CLOSE-WAIT

   FIN-WAIT-2 <--<SEQ=Y><ACK=X+1><CTL= ACK>   <--CLOSE-WAIT

                                                       (被动关闭)

   TIME-WAIT <--<SEQ=Y><ACK=X+1><CTL=FIN,ACK><--LAST-ACK

   TIME-WAIT --><SEQ=X+1><ACK=Y+1><CTL=FIN,ACK>--> CLOSED

     (等待2MSL)

   CLOSED

 

       IP数据包格式,略

       ICMP协议产生的控制报文放在IP数据包里,通过IP数据包发送到制定地点。

 

      

ACK q+1

FIN q

ACK p

FIN p

………………

DATA, 带回确认

DATA, 带回确认

DATA

ACK=b+1

SYN=b, ACK=a+1,

MSS=1460

SYN=a, MSS=1460

一个正常的TCP通信过程

connect主动请求发送后状态:SYN-SEND

Client

Server

LISTEN正在倾听accept

发送后状态SYN-RVCD

ESTABLISH

connect返回

ESTABLISH

accept返回

write数据

read数据,

处理数据,

write写回结果

read数据

close主动要求关闭连接:

FIN-WAIT-1

被动关闭

CLOSE-WAIT

FIN-WAIT-2

(等待2MSL)

CLOSED

TIME-WAIT

close

LAST-ACK

CLOSED

TCP最初需要进行MSS(Maxium Segment Size)协商,否则数据包可能被分段后再重组

 

 

 

 

 

 

 

 

第四章  基本套接字编程

·4.1 基本套接字函数族

头文件:<sys/types.h>, <sys/socket.h>

主要函数:

int socket(int domain, int type, int protocol);              //创建socket描述符

[domain=AF_UNIX,AF_INET,AF_ISO; type=SOCK_STREAM,SOCK_DGRAM,SOCK_RAW;]

 

int connect(int sockfd, struct sockaddr* servaddr, int addrlen);  //向服务器发送连接请求

int bind(int sockfd, struct sockaddr* myaddr, int addrlen);     //向系统登记一个固定端口

int listen(int sockfd, int backlog);                                        //被动倾听套接字的指定端口

int accept(int sockfd, struct sockaddr* addr, int addrlen);  //从完全连接队列接收一个连接套接

[如果完全连接队列为空队列则堵塞,或直接返回-1accept成功返回一个(继承倾听套接字属性的)连接套接字描述符]

int close(int sockfd);                                         //关闭套接字描述符

int shutdown(int sockfd, int howto);                             //关闭套接字描述符读写信道

[howto = SHUT_RD, SHUT_WR, SHUT_RDWR]

 

网络字序转换函数族:

头文件:<netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);            //host to network long 字序转换

unsigned long int htons(unsigned long int hostlong);           //host to network short 字序转换

unsigned long int ntohl(unsigned long int hostlong);            //network to host long 字序转换

unsigned long int ntohs(unsigned long int hostlong);      //network to host short 字序转换

 

IP地址转换函数族

头文件:<sys/socket.h>, <netinet/in.h>, <arpa/inet.h>

主要函数:

int inet_aton(const char *cp, struct in_addr *inp);      //将字符串表示IP地址转struct in_addr表示

unsigned long int inet_addr(const char *cp);    //将字符串表示IP地址转32 bits表示

char *inet_ntoa(struct in_addr in);                  //struct in_addr表示IP地址转字符串表示

 

服务器应答

客户发送数据

客户请求建立连接

socket()

套接字编程基本流程

bind()

listen()

accept()

堵塞等待请求

read()

write()

close()

socket()

connect()

write()

read()

close()

Client

Server

NOTE:

通常客户端不需要bind端口,系统动态分配

示例代码:

listen_fd = socket(AF_INET, SOCK_STREAM, 0);  //创建Internet协议簇,流类型套接字

if (listen_fd == -1)        error_proc();

bzero(&serv_addr, sizeof(serv_addr)); //初始化服务器地址结构

serv_addr.sin_family = AF_INET;   //使用Internet协议簇

serv_addr.sin_port = htons(端口号);  //设置端口号,并转换为网络字序

/*如果是客户端执行

 *ret = inet_aton(“127.0.0.1”, &serv_addr.sin_addr); //socketaddr地址结构设置IP地址

 *然后bind();指定端口,接着connect(); 服务器端请求建立连接,开始“三次握手”协议

 *如果是服务器端则执行  */

serv_addr.sinaddr.s_addr = htonl(INADDR_ANY); //允许任何网络设备接口连接并处理

ret = bind(listen_fd, (struct sockaddr*)&serv_addr,sizeof(serv_addr));  //套接字绑定指定端口

if (ret < 0) error_proc();

listen(listen_fd, 倾听队列长度);    //转为倾听套接字,设置倾听队列长度

//从完全连接队列中接受一个新连接,返回连接套接字描述符

conn_fd = accept(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

 

·并发服务器模式

  /*

       0 服务器端套接字初始化,

       1服务器端的父进程用倾听套接字X,倾听来自客户端的请求连接,

2accept一个连接套接字Y时候fork子进程专门处理客户端的数据处理,

3子进程关闭倾听套接字X不使用,处理客户端请求,完毕后关闭Y,退出,

4而父进程关闭连接套接字描述符Y不使用,继续倾听新的客户端连接(转过程2)

   */

do{

       /*过程2; 3; 4; 的实现参考代码*/

//从完全倾听队列中接收一个连接套接字描述符

       conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));

       if (conn_fd < 0)

error_proc();        //错误处理

       switch ( ret = fork() ){

       case -1:

              error_proc();        //错误处理

    case 0:

              close(listen_fd);      //关闭倾听套接字描述符

              serv_for(conn_fd);    //为客户端提供服务

              exit(0);

default:

       close(conn_fd);       //关闭连接套接字描述符

}

} while (continue);

 

 

 

第五章  无堵塞套接字和单进程轮询服务器

·5.1 无堵塞套接字

       堵塞套接字在等待输入/输出时会进入睡眠,不能继续其他的操作。在并发服务器模式下这一缺点并不明显,但在一些复杂应用中可能需要在单进程中为多个连接服务,这时堵塞套接字会大大降低效率。另外,进程可能一直被堵塞。比如服务器端崩溃,而客户端并不知道,此时客户端进程将一直堵塞。

       无堵塞套接字会对读、写、建立连接、接收连接过程产生影响。总的来说就是不等待所有资源到齐,而立即操作并返回,这点在和处理无堵塞套接字上有一些区别。如果本已堵塞而由于使用无堵塞套接字,那么errno将返回EWOULDBLOCK,通过下面语句可以判断:

ret = accept(...);

if (ret < 0 & errno != EWOULDBLOCK) //如果错误返回并且错误原因不是无堵塞

 

       无堵塞套接字的两种实现:

int (flags = fcntl(sock_fd, F_GETFL, 0) < 0) error_proc();

flags |= O_NONBLOCK;

if (fcntl(sock_fd, F_SETFL, flags) < 0) error_proc();  //这种方法是POSIX标准定义方式

 

int b_on = 1;  //ioctl函数中使用FIONBIO命令

ioctl(sock_fd, FIONBIO,&b_on);

 

·5.2 单进程轮询服务器模式

make_null(serv_slot, maxlen);  //serv_slot[]是连接套接字描述符数组,本进程为其提供服务

listen(listen_fd, MAXSIZE);   //建立倾听套接字

do{

//从完全倾听队列中接收一个连接套接字描述符

       conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));

       if (conn_fd < 0 && errno != EWOULDBLOCK)

error_proc();                     //错误处理

       else  if (conn_fd >= 0)                //接收到新的连接套接字描述符

              create_new_connect(conn_fd, serv_slot, &maxlen);   //建立新连接

for (i = 0; i < maxlen; ++i)      //0maxlen都是有效连接,进程轮流为其服务

        serve_for(serv_slot[i]);             //本进程为第i个连接服务

} while (continue);

       使用单进程轮询服务器模式仍然无法避免客户端的某些意外(比如非正常断线)或恶意行为造成失效,而且如果客户数量增加,服务器端的相应时延也会加大。因此我们仍然使用并发服务器模式来提供并行的服务,因为一个服务器子进程失效不会影响到其他进程的工作。

 

 

第六章  带外数据与多路复用、信号驱动的输入/输出模型

·6.1 多路复用的输入/输出模型

       多路复用的概念:进程不是主动询问套接字情况,而是希望对监视的套接字向系统登记,而后采用被动的态度等待。当监视的套接字上发生了事件,进程去检查发生的状况然后做相应的处理。在这种工作方式下,进程是在已经知道在套接字上发生了事件才去检测,在没有发生事件的时候进入睡眠状态。

 

头文件:<sys/time.h> <unistd.h>  [<signal.h>(pselect使用)]         

主要函数:

int select (int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timeval *timeout);

[maxfd是需要监视的最大文件描述符值+1,即系统监视从0maxfd-1的文件描述符;

rdset, wrset, exset是对应需要检测的可读、可写和异常文件描述符集合;

timeout内没有发生事件,函数返回0]

 

int pselect(int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timespec *timeout,const sigset_t sigmask);  //POSIX中对select函数的增强,参数sigmask是执行后对堵塞信号恢复

 

文件描述符集合:

FD_ZERO(fd_set *fdset);            //清空初始化

FD_SET(int fd, fd_set *fdset);        //增加

FD_CLR(int fd, fd_set *fdset);        //删除

FD_ISSET(int fd, fd_set *fdset);       //判断包含

 

·套接字的读、写和异常就绪条件

       读就绪:倾听套接字完全连接队列建立新连接;连接套接字的读缓冲区超过读下限、读管道关闭和套接字异常。

       写就绪:连接套接字的写缓冲区空闲小于某下限、写管道被关闭和套接字异常。

       异常就绪:套接字上到达外带数据。异常就绪连带触发读、写就绪。

上面列出部分常用就绪条件,具体参考帮助手册。

 

基本用法:

FD_ZERO(&r_set);               //初始化

FD_SET(listen_fd, &r_set);         //加入可读文件描述符集合

ret = select(listen_fd+1, &r_set, NULL, NULL, NULL); //对倾听套接字进行就绪判断

 

 

·6.2 信号驱动的输入/输出模型

       信号驱动通常用于接收紧急数据。进程先向系统登记,然后系统检测到数据到达后会向接收者发生SIGIO信号,然后接收者在信号处理器中接收数据。这种方式通常用在接收紧急的控制数据场合。

 

数据接收者设置:

#include <fcntl.h>

int fcntl(int fd, int cmd,...);  

//使用命令F_SETOWN,第三个参数如果是正整数表示进程号,负整数表示进程组接收

 

 

·6.3 系统I/O模型的总结

       本书讲述了“堵塞方式、非堵塞方式、多路复用和信号驱动”四种I/O模型。

       1 堵塞方式:

              广泛使用在并发服务器上,当套接字不满足操作条件立即堵塞等待资源。

       2 非堵塞方式:

              广泛使用在单进程轮询服务器上,浪费较大CPU资源使用场合较少。

       3 多路复用方式:

              广泛使用在单进程进行多客户端服务上,比非堵塞方式在轮询中节约CPU时间。

       4 信号驱动方式:

              广泛使用在接收紧急数据场合。

 

·6.4 带外数据的接收和发送

       带外数据就是指在正常数据流信道之外传输的数据,通常用在对远端进程的同步和控制。它和信号驱动方式几乎相同,但是发送的是SIGURG信号,不是SIGIO

 

头文件:<sys/types.h>, <sys/socket.h>

主要函数:

int send(int sockfd, void *buf, int len, int flags);  //使用MSG_OOB控制选项发送带外数据

int recv(int sockfd, void *buf, int len, int flags);  //使用MSG_OOB控制选项接收带外数据

 

       带外数据一次只允许发送一个字节,如send(sock_fd, “bc”, 2, MSG_OOB)TCP只认为最后一个是带外数据,之前都是普通数据。在特殊情况下,带外数据包优先被接收方接受。

接收方在缺省情况下(使用ioctl函数可以改变)使用一个字节的外带数据缓冲区接收外带数据,并且外带数据段和普通套接字数据段字符集不同,可以区分外带数据和普通数据。     

如果接收方收到多个带外数据段,TCP会和先前一次收到的数据段中数据做比较,如果其值相同则认为它们是同个带外数据段。由于发送方可能发送多个外带数据段,接收外带数据是必须做容错处理。

接收方同一时刻只允许有一个字节的外带数据,先到者如果没有被及时处理,那么任何后来的外带数据段都将覆盖它。带外数据被覆盖后成为普通数据。

       收到外带数据将触发异常就绪。直到读指针大于带外数据标示(紧急)指针后解除异常。

       服务器端接收外带数据可以使用:

1 多路复用方式,要点:

检测异常就绪和读就绪套接字先后顺序不同,结果也不同。

2 异步信号驱动方式,要点:

设计SIGURG信号处理器,处理前后注意屏蔽/堵塞信号。

3 检测带外数据标记方式,要点:

       //套接字设置成SO_OOBINLINE,即外带数据看成普通数据存放,on=1

  setsockopt(conn_fd,SOL_SOCKET,SO_OOBINLINE,&on,sizeof(on));

  ioctrl(conn_fd, SIOCATMARK,&n_data); //检测读指针是否和带外数据标示指针重合

  if (n_data == 1) //带外数据到达

 

注意:被覆盖的带外数据将保留继续保留在读缓冲区里,而后当成普通数据读入。如果使用过的带外数据没有及时得从缓冲区里删除,该带外数据可能会被当场普通数据读入,如sleep()系统调用可能导致这类需要紧急处理的过程产生诡异的行为!

 

 

 

 

第七章 UDP数据报

·7.1  UDP数据报

UDP源端口、UDP目的端口 标示进程;UDP数据报长度;校验和,可选。

UDP非面向连接,不可靠传输,具有较小传输时延。

 

·7.2  UDP传输过程

SERVER: socket() --> bind() --> recvfrom() --> sendto()-->close()

CLIENT : socket() --> sendto() --> recvfrom() --> close() 

服务器端和TCP协议相比少了listen()accept()两个过程,而客户端不需要建立连接

 

       头文件:<socket.h>

       发送和接收函数:

       int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);      //参数to是指定接收方地址

       int recvfrom(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *from, socklen_t *addrlen);   //参数from是保存发送方的地址

       int send(int sockfd, void *buf, int flags);      //使用connect绑定后,发送到缺省地址

       int recv(int sockfd, void *buf, int flags);      //使用connect绑定后,从缺省地址接收

    //socket(AF_INET, SOCK_DGRAM, 0)  创建一个UDP套接字

 

·7.3  UDP服务器和TCP服务器的比较

       使用UDP协议的服务器通常是非面向连接的,因此不用listenaccept的。UDP服务器只需要在其端口上等待客户机发来的数据报即可。

       TCP服务器需要和客户端进行连接然后,独占一个连接套接字为其服务;而UDP服务器实际并不和客户机进行连接,UDP服务器仅是接收报文,处理并返回结果。

       UDP协议并不关心数据报的可靠性和次序,而希望应用程序保证这些。

       TCP服务器中,服务器可以调用getsockpeer函数获取客户机地址信息和端口号,使用getsockname获取套接字对应的IP地址和端口号。

UDP服务器中,可以在recvfrom函数中获取数据报的源地址,使用getsockname获取数据报的端口。如果该UDP服务器有多个IP地址,则无法判断是哪个IP地址获取该数据报。因为UDP协议非面向连接,没有记录接收方的IP地址,此时如果需要明确知道是哪个IP地址收到该数据报,在服务器端需要为每个建立多个UDP套接字绑定在不同网络接口上,通过getsockname获取对应的IP号。

 

·7.4  UDP的“连接”

       UDP是非面向连接,但是也可以调用connect函数对套接字进行绑定到缺省IP地址上。

       UDP服务器调用conncet后,并不启用“三次握手”,仅仅记住目的地址和端口。在使用send函数时候会自动按照缺省情况来填写数据报头;同时也可以用sendto函数发送指定位置。

但在接收时候,如果套接字已绑定一个地址和端口,那么UDP服务器只接收该套接字上源地址和端口相同的数据报。如果一个数据报源地址和端口和该套接字设置不同,则丢弃它。而没有绑定地址和端口的UDP套接字可以接收任意来源的数据报。

可以多次调用connect函数修改UDP套接字的绑定地址和端口设置。如果调用connect(AF_UNSPEC, NULL, 0)可以取消对套接字的绑定。

 

·7.4 UDP应用程序性能改进

       1 解决报文的无序问题:

UDP报文设计一个数据序列号,接收到报文先进行排序后再转交给数据处理程序。

       2 解决报文的流量控制问题:

  如果客户端接收能力远高于服务器端发送速率,则会让网络传输性能大大下降;

如果客户端接收能力远低于服务器端发送速率,则会让UDP报文大量丢失,消耗服务器端资源。因此需要匹配双方的发送和接收速率,有利用提高整体性能。我们需要在应用程序中建立一种端对端的流量控制反馈机制,由客户端给服务器端发送接收能力的反馈,让服务器端动态调整发送速率。

应用缓冲区

核心缓冲区

应用程序处理套接字缓冲的速率

应用程序读取套接字缓冲的速率

应用程序发送缓冲区

应用程序接收缓冲区

单个系统发送缓冲区

UDP套接字接收缓冲区

服务器端发送速率

根据客户端程序接收缓冲的占用情况给服务器端提供反馈

服务器端

客户端

 

 

第八章  域名系统和通用套接字选项

·8.1 域名系统

#include <netdb.h>

struct hostent *gethostbyname(const char *hostname); //指定域名地址来获取IP地址

struct hostent *gethostbyaddr(const char *addr, size_t len, int family); //指定IP地址获取域名

struct hostent{

       char *h_name;              //主机名

       char **h_alias;       //主机别名列表

       int h_addrtype;       //主机地址类型

       int h_length;           //主机地址长度

       char **h_addr_list;//主机IP地址列表

};

 

int gethostname(char *name, size_t len);  //返回本机域名地址 <unistd.h>

int uname(struct utsname *name);  //同上,#Include <sys/utsname.h>

struct servent *getservbyname(const char *servname, const char *protoname);//服务名获取端口

struct servent *getservbyport(int port, const char *protoname);  //由端口名获取服务信息

 

·8.2 套接字选项

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *potval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname, cosnt void *potval, socklen_t *optlen);

[通用套接字选项,参考手册,略。]

 

 

第九章 高级套接字函数编程

·9.1 发送和接收函数的高级用法

头文件:<sys/types.h>, <sys/socket.h>

int send(int sockfd, void *buf, int len, int flags);

[flags=MSG_OOB, MSG_DONTWAIT, MSG_DONTROUTE] 

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

[flags=MSG_OOB, MSG_PEEK, MSG_WAITALL, MSG_DONTROUTE]

 

 

头文件:<sys/uio.h>

int readv(int fd, struct iovec *iov, int iovlen);   //将套接字缓冲区数据读到多个应用缓冲区中

int writev(int fd, struct iovec *iov, int iovlen);  //将多个应用缓冲区写到套接字缓冲区数据中

struct iovec{

  void *iov_base;  .//指向应用缓冲区结构体的数组

  size_t iov_len;   //缓冲区的个数

};

 

头文件:<sys/types.h>, <sys/socket.h>

int recvmsg(int sockfd, struct msghdr *msg, int flag);   //常用在UNIX域套接字中对

int sendmsg(int sockfd, struct msghdr *msg, int flag);  //进程间发送/接收文件描述符使用

struct msghdr{

       void *msg_name;      //发送端的地址信息

       int msg_namelen;      //  

       struct iovec *msg_iov;  //缓冲区结构体指针

       int msg_iovlen;       //缓冲区个数

       void *msg_control;    //控制信息

       int msg_controllen;    //控制信息长度

       int msg_flags;       

};

使用这两个函数发送附加消息时候通常需要定义以下结构体:

union{

       struct cmsghdr cm;

       char control[CMSG_SPACE(sizeof(附加段长度))];

};

操作宏,头文件: <sys/socket.h>, <sys/param.h>

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msghdrptr);//指向第一个cmsghdr结构指针

struct cmsghdr *CMSG_NEXTHDR(struct msghdr *msghdrptr, struct cmsghdr *cmsgptr);

unsigned char *CMSG_DATA(struct cmsghdr *cmsghdr); //返回指向cmsghdr结构第一个字节

unsigned CMSG_LEN(unsigned int lenght);  //获取cmsghdr中存放数据字节数

 

 

 

 

 

第十章 守护进程和超级服务器inetd

·10.1 守护进程的原理

       只要系统没有关机或者崩溃,守护进程将在系统中不间断运行。关键是如何把守护进程的运行环境和其他进程的运行环境隔离。步骤:

1 第一次forksetsid函数调用建立新会话组

       这个操作的主要目的是为了让一个进程和控制终端脱离,这样来自终端的信号就不会影响到守护进程。setsid()函数的功能是让创建一个新的会话过程,并让调用setsid()的进程成为该会话过程的领头进程。但是setsid()的调用条件是这个进程不是一个进程组的主进程。因此我们先fork()一个子进程并终止该进程组主进程的运行,在子进程中调用setsid()让子进程成为不带控制终端的新会话过程的领头进程。于是这个进程就和原控制终端脱离,并成为新会话组的领导进程。

 

2 第二次forksetpgrp函数调用建立新进程组

       调用setsid()之后,虽然在新的会话组中的领导进程不带控制终端。但是如果这个进程打开了一个终端,那么整个会话组将又重新的控制终端。因此我们需要fork()一个新的子进程继续运行,并终止该会话组领头进程的运行。此时这个新进程不是会话组领导进程,所以即使打开一个终端也不会成为整个会话组的控制终端。

但由于父进程是会话组的领头进程,如果让父进程退出而子进程继续运行,那么将发送SIGHUP(中断和挂起信号)给这个会话组中所有进程。因此我们要先忽略信号SIGHUP,然后再fork,接着退出父进程而子进程继续运行。

但是这个新进程仍然和已退出父进程同在一个进程组中,仍然会受到同个进程组中的信号影响。所以我们要使用setpgrp()让这个进程成为新的进程组的领导者。

 

3 关闭所有文件描述符

       fork()时候子进程将继承父进程的文件描述符。我们需要在守护进程中关闭先前打开的文件描述符,以免对其他进程造成影响。使用sysconf(_SC_OPEN_MAX)函数获取系统中每个进程可以打开文件的最大数目,然后使用close()关闭它们。

 

4 消除umask的影响

       每个进程都有一个umask同它关联。umask指定进程创建文件的保护掩码,是系统提供的一种安全机制,限制进程创建文件的权限。比如进程创建一个文件的权限为0777,进程umask0277,那么实际文件权限为0777-0277=0500。此时如果其他进程去读写守护进程创建的文件可能会受到文件掩码的影响。使用umask(0)系统调用清除旧的文件掩码。

 

5 改变守护进程的当前目录

       每个进程都有一个当前目录,当进程产生错误时可以将错误信息记录在当前目录的core文件中供以后分析错误使用。如果不把守护进程修改到一个安全的目录,那么守护进程的当前目录是不确定的,并可能影响到其他系统的管理工作。我们可以使用chdir(“/”);将守护进程当前目录修改到根目录下。

 

6 对标准I/O描述符重定向

       由于守护进程不连接任何控制终端,并且所有文件描述符都关闭。那么如果意外调用printf, perror等输出将导致出错。我们把标准I/O重定向到无伤害设备(harmless device)描述符上,那么对标准I/O的操作都将忽略避免人为意外使用出错。

 

7 使用syslog记录守护进程的错误

       syslogd本身就是一个守护进程,使用UDP 514端口。应用程序可以发送UDP报文记录错误信息。void syslog(int priority, const char *message, ...); //#incluse <syslog.h>

 

8 文件锁和控制守护进程副本互斥运行

       为了避免运行多个相同的守护进程产生的干扰,因此使用锁文件的方式记录守护进程的工作情况。linux系统的锁采用咨询锁,只是系统将告知文件锁定情况而不阻止进程对文件的操作。我们可以使用int flock(int fd, int operation); //#include <sys/file.h>

[operation = LOCK_SH共享锁, LOCK_EX互斥锁,LOCK_UN解锁,LOCK_NB进程获取锁失败不堵塞,缺省为进程堵塞]

 

 

示例代码:

int init_daemon(const char *pathname, int facility);

{

       struct sigaction act;

       int max_fd, i, ret;

       int lock_fd;

       char buf[10];

       //打开一个文件锁

       lock_fd = open(LOCKFILE, O_RDWR | O_CREAT, 0640);

       if (lock_fd < 0)

              error_proc();

       //加锁

       ret = flock(lock_fd, LOCK_EX LOCK_NB);

       if (ret < 0)

              error_proc();

       //第一次fork

       ret = fork();

       if (ret < 0)

              error_proc();

       else if (ret != 0)

              exit(0);    //关闭进程组的主进程,子进程继续运行      

       ret = setsid();  //这个子进程成为新会话组的领导进程

       if (ret < 0)

              error_proc();

       //忽略SIG_IGN

       act.sa_handler = SIG_IGN;

       sigemptyset(&act.sa_mask);

       act.sa_flag = 0;

       sigaction(SIGHUP, &act, NULL);

       //第二次fork

       ret = fork();

       if (ret < 0)

              error_proc();

       else

              exit(0);  //关闭进程组的主进程,子进程继续运行

       chdir(“/”);    //改变守护进程的当前目录

       umask(0);    //清除旧的文件掩码

       setpgrp();    //让这个进程成为新进程组的领导进程

       sprintf(buf, “%6d/n”, getpid());  //获取守护进程的ID号,保存进buf数组中

       write(lock_fd, buf, strlen(buf));  //把守护进程ID号保存进锁文件中

       max_fd = sysconf(_SC_OPEN_MAX);   //获取进程最大打开的描述符

       for (i = 0; i < max_fd; ++i)

              close(i);                                        //逐个关闭

       open(“dev/null”, O_RDWR);          //打开 无伤害设备

       dup(1);    //

       dup(2);      //标准I/O描述符重定向

       openlog(pathname, LOG_PID,facility);  //打开syslogd记录文件

       return 0;   //守护进程标准创建过程结束

}

 

·10.2 超级服务器inetd的工作原理

       由于服务器套接字初始化方式非常类似,所以可以设计一个专门的服务器负责初始化工作,并且它将根据接入端口不同调用相应的服务程序进行工作,这些服务程序在未被接入前都处于睡眠等待状态。采用超级服务器的方式可以让服务器程序采用统一方式管理。

 

       超级服务器将采用select的方式并发检测在文件/etc/inetd.conf中说明的TCP/UDP端口,一旦发现有客户接入就创建一个子进程。超级服务器inetd是服务接入者,它在创建字进程时候调用exec()载入具体的服务程序。在子进程中关闭倾听套接字,父进程中关闭连接套接字,于是父进程继续检测,子进程开始为客户端进行服务。对于wait服务程序,超级服务器inet载入它时候将其在检测集合中删除,等待该服务结束后才能接入下次服务。服务程序完毕后将发送SIGCHLD信号,超级服务器将其继续加入检测集合。当系统管理员修改超级服务器配置文件后将发送SIGHUP信号,超级服务器将重新初始化。

      

 

 

第十一章  数据结构的传输和XDR标准

·11.1 数据结构的传送

       网络数据结构传递可能存在以下问题:网络字序问题、浮点数传输、指针处理

       自定义手工处理方式:

将待发送数据结构转换以后放入应用的发送缓冲区;

将应用的接收缓冲区中数据结构转换以后再进行数据处理。

       代码示例:

       void send_int32_2buf(char *buf, unit32_t n)

       {

              n = htonl(n);

              bcopy((char *)&n, buf, sizeof(unit32_t));

       }

       void send_string_2buf(char *buf, char*str);

       {

              bcopy(str, buf, strlen(str));

       }

       void send2_buf(char *buf, struct u_data *ptr)

       {

              send_int32_2buf(buf, ptr->aInt);

              buf += sizeof(unit32_t);

              send_string_2buf(buf, ptr->str);

              buf += strlen(ptr->str); // * sizeof(char)

}

void recv_int32_from(char *buf, unit32_t *n)

{

       bcopy(buf, (void*)n, sizeof(unit32_t));

       *n = ntohl(*n);

}

void recv_from_buf(char *buf, struct u_data *ptr)

{

       recv_int32_from_buf(buf, &(ptr->aInt));

       buf += sizeof(uint32_t);

}

 

·XDR标准和实现原理

XDR数据结构传输标准是SUN公司设计的,已经成为大多数客户机/服务器应用中的事实上的标准。

      

XDR对各种数据类型规定了编码方式。初始化函数是

#include <rpc/xdr.h>

extern void xdrmem_create((XDR *xdrs, const  caddr_t addr, u_int size, enum xdr_op xop)); //xdrs是创建后XDR流指针,addr是存放XDR流发送缓冲区

       XDR的流转换方式和上面自定义方式类似,但是对各种数据类型的处理做了统一规定。应用程序设计配对的接收和发送,分别处理每个数据项。

 

       XDR有内存流和I/O流两种。可以使用内存流,进行套接字缓冲区间的数据结构传输;使用I/O流将编解码的结果输出到文件流中。

 

       XDRTCP都是流的抽象,所以两者可以很好结合。另外XDR提供了面向记录的XDR抽象,应用在UDP传输。

 

       XDR转化函数所做操作决定于XDR流本身性质。如果XDR是编码流时,转化函数就做数据编码,如果是解码流时,转换函数就做数据解码。

 

第十二章  RPC远程过程调用原理和实现

·RPC的原理

       使用XDR协议可以让数据结构无差别的在网络传输,使用RPC(remote procedure call)远程过程调用可以使函数在不同主机上运行。RPC所要达到目的是将网络通信功能和应用的需求分开。

       RPC的中心是优先考虑应用的分析,在模块功能划分完毕后将其分离出来。这些模块在不同主机上运行,RPC保证模块分离前后的语义不变。

      

函数调用的4个原则:

1 把函数所需参数准备好,通过某种方式可以让调用函数访问到。

2 必须包含函数的返回信息

3 能够确定调用函数的位置

4 为调用函数创建可以运行的环境

 

其中“本地调用”这是上述四个原则的一种实现,1 通过堆栈段 2 函数返回地址 3 PC指针指向函数入口地址 4 局部变量在函数栈分配,其他数据共享进程数据段

 

RPC远程调用模拟本地调用:

我们可以使用定义好的信息格式保存调用过程参数,在信息格式中说明如何去找被调用者(通常是某种标志)。然后通过网络将信息报文发送到被调用所在机器上,然后调用者等待被调用方发回的调用结果。 【条件13

被调用主机上应有一个分派器控制所有远程调用过程。收到报文后通过其中的标志知道需要调用哪个,取出被调用者需要的参数然后传入。【条件2

被调用过程在它所在环境中运行,并将运行结果写在信息格式中,最后网络将调用结果返回。【条件4

准备函数传入参数

说明函数如何返回

确定被调用函数位置

创建被调用函数环境

被调用函数运行

准备运行结果

切换回调用者的环境

返回被调用者的结果

·RPC的实现

远程过程标示:(程序号,远程调用过程版本号,远程过程序号)

 

端口的动态映射:每一个远程调用过程都对应占用一个有操作系统动态分配的传输层端口。调用方需要调用一个远程调用过程,它会向端口映射器发送一个请求,然后端口映射器通过查表返回相应远程调用过程的端口号。然后它向远程调用过程发起调用请求。

 

RPC的报文:RPC使用XDR语言定义应用的报文。

RPC开发工具:由于ONC RPC协议规程非常复杂,因此系统提供了专门用于开发RPC的工具。注意包括:XDR库函数,RPC运行时间库函数,一些程序的自动生成工具,产生一个构件RPC分布式程序需要的C程序文件,这些程序主要是屏蔽底层通信对应用的影响。

 

客户端主要实现:

       向端口映射器发送请求,并从端口映射器接收回应;形成CALL报文,向真正的远程调用过程发送调用请求,接收来自服务器的调用结果。

服务器段主要实现:

       在提供服务之前向端口映射器注册自己实际端口;将一个调用分派到具体调用程序中的一个调用过程。

 

 

 

第十三章 UNIX域套接字和并发服务器的预创建技术

·UNIX域套接字

linux操作系统提供了一种UNIX域协议的进程间通信方式,它不能应用在网络中,能使用在本机两进程间的通信中。它能方便的向两个非亲属关系的进程间传递文件描述符,效果类似于在父子进程间传递一样。UNIX域套接字在和本地进程进行交互时候效率更高,因为它不需要处理网络异常可能。

 

地址结构:

struct sockaddr_un{

       short int sun_family;

       char sun_path[104];

};

使用示例:

listen_fd = socket(AF_UNIX,SOCK_STREAM,0);           //UNIX域套接字

serv_addr.sun_family = AF_UNIX;                        //UNIX域协议簇

strcpy(serv_addr.sun_path, “/tmp/myfile”);                  //创建绑定文件

unbind(serv_addr.sun_path);                             //先解除指定文件的绑定

ret = bind(listen_fd,(struct sockaddr*)&serv_addr,

strlen(serv_addr.sun_path)+sizeof(serv_add.sun_family);   //绑定指定文件路径

       listen(listen_fd, 倾听队列长度);

 

·使用sendmsgrecvmsg传递和接收文件描述符

       主要使用UNIX域套接字,sendmsgrecvmsg两个函数实现。在发送附加数据之前需要自己定义一个联合体。这样附加数据将在内存中置于struct cmsghdr结构之后,同cmsghdr相关宏定义才能正常工作。

示例代码:

void unix_send_fd(int unix_fd, int conn_fd)

{

       struct msghdr msg;

       struct iovec iov[1];

       char c;

       int ret;

       union{

              struct cmsghdr cm;

              char control[CMSG_SPACE(sizeof(int))];

       }control_un; //定义联合体,将附加数据置于struct cmsghdr结构之后

       struct cmsghdr *cmptr;

       //报文套接字使用字段,对于流类型套接字,地址域填为空。

       msg.msg_name = NULL;

       msg.msg_namelen = 0;

       //填写普通数据的缓冲区

       iov[0].iov_base = &c;

       iov[0].iov_len = 1;

       //执行普通数据缓冲区

       msg.msg_iov = iov;

       msg.msg_iovlen = 1;

       //设置附加数据的指针

       msg.msg_control = control_un.control;

       msg.msg_controllen = sizeof(control_un.control);

       //获取结构struct cmsghdr的指针

       cmptr = CMSG_FIRSTHDR(&msg);

       //获取该结构的长度

       cmptr->cmsg_len = CMSG_LEN(sizeof(int));

       //填写控制数据的层次

       cmptr->cmsg_level = SOL_SOCKET;

       //填写控制数据的类型

       cmptr->cmsg_type = SCM_RIGHTS;

       //填写控制数据的内容

       *(int*)CMSG_DATA(cmptr)=conn_fd;

       ret = sendmsg(unix_fd, &msg,0);

       if (ret < 0)

              error_proc();

       //接收就是上面逆向的过程,先预设接收缓冲,然后cmptr= CMSG_FIRSTHDR(&msg);

       //最后return *(int*)CMSG_DATA(cmptr);

}

 

·并发服务器的预创建技术

       预创建技术可以为并发服务器解决以下几个显著缺点:

       1 为新连接上的客户尽快提供服务,消除新建进程的时延

       2 不必为每个客户服务进程都需要建立/释放资源

       3 子进程重复利用率高

       4 通过传递文件描述符,对子进程动态管理

 

       UNIX套接字对

       int socketpari(int family, int type, int protocol, int sockfd[2]);

       //建立两个连接好的套接字对,可以像管道一样使用

       使用套接字和管道相比有“全双工、可以将文件描述符传递给任何进程”的优点

 

 

第十四章 原始套接字

·14.1 原始套接字

      

类型为0的原始套接字

ICMP类型原始套接字

EGP类型原始套接字

ICMP协议实体

EGP协议实体

UDP/TCP处理模块

UDP/TCPIP

承载ICMP报文IP

承载EGP报文IP

       原始套接字的使用:

       1  ip_fd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); //只允许超级用户建立和使用

              //SOCK_RAW创建原始套接字,第三个参数是协议,可以使用0或其他

 

2         bindconnect函数对原始套接字的影响

原始套接字工作在传输层以下所以没有端口的概念。bind一个原始套接字的地址后,核心只把目的地址是该原始套接字的IP包发送给它,忽略其它。调用connect后,核心将传递源地址是connect地址的IP包给这个原始套接字。如果没有bindconnect原始套接字,核心将把所有协议匹配的IP包传递给它。

      

IP套接字选项

       getsockopt()函数:IP_TTL获取生存期,IP_HDRINCL自行填充IP包头(可伪造)

 

第十五章  多线程编程

·15.1 线程的概念

线程这样一种实体,它被包含在进程实体中,具有自己的运行线索,可以完成一定的任务。它和进程中其他进程共享所有的共享数据以及部分环境,并且可以和其他线程协同完成一定任务。线程常常被成为轻量级进程。

 

多个线程将共享同个进程虚空间的环境包括代码段和大部分数据,所以fork需要建立的大量复制创建就不需要,不同线程可以通过共享变量进行数据传输,不需要复杂的IPC机制。

 

同个进程的线程将并发运行。线程有独立的:线程ID、寄存器组值、线程堆栈、错误返回码变量errno、线程信号屏蔽码、线程优先级。

线程ID

32位屏蔽码

优先级

信号

调度器

进程虚空间

 

....................

....................

....................

 

大部分数据

代码段

程序计数器

堆栈基址指针

堆栈栈顶指针

    .......

寄存器组

 

 

·15.2 线程的分类

1 用户线程:

用户线程的实现是通过运行时间系统代码来实现。这部分代码的功能是将线程恢复运行时需要保存的寄存器组信息和其他信息保存在一个结构中,然后将这个结构放在线程调度队列中,然后在线程队列中选择一个优先级最高的线程,而后将结构中保存的信息回复运行这个线程。

 

2 内核线程:

       内核线程切换是通过内核调度器来实现,和普通进程一样是核心调度器实体。

 

·15.3 用户线程和内核线程的比较

       1 CPU时间的分配

用户线程是通过在进程中连接用户级线程包来实现,所以这个进程的多个线程将竞争CPU分配给这个进程的时间。用户级线程不能在多个CPU环境中同时运行

内核线程和其他进程一起在核心调度器中竞争CPU时间,可以在多CPU环境运行更具良好的并发性

      

2 线程的并发性

除非一个运行中的用户级线程放弃CPU时间去运行进程内的线程切换代码,否则其他线程将不能获取CPU时间。因此一些慢系统调用将引起线程堵塞,而其他线程却无法切换。因此在用户级线程中,可能堵塞的系统调用将被无堵塞的版本取代。也因此这些原因和CPU的时间分配,大大限制了用户级线程的并发性

内核线程在发生堵塞时候,将由核心调度器把CPU时间分配给其他进程或核心线程。

 

3线程调度的开销

     用户级线程的调度在进程内所有开销很小;内核线程调度和进程调度类似,所以开销较大。内核线程的良好并发性是用较大的开销换取,在对于并发性要求不是很高的应用时,用户级线程更加合适。

 

·线程函数库的使用

      

·线程同步

       采用无名信号量、互斥锁、条件变量和信号变量、信号处理器等。

·线程应用

       建议深入阅读其他专著。略~

 

 

原创粉丝点击