UNIX网络编程卷一:套接字联网API-整理

来源:互联网 发布:counter python 编辑:程序博客网 时间:2024/06/06 02:24

UDP部分

p196

UDP的connnect函数

UDP调用connect后,内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的IP地址和端口号,然后立即返回到调用进程。

相对于未connected的UDP套接字,发生了三个变化:

可以对UDP套接字多次调用connect

1)指定新的IP地址和端口号;

2)断开套接字;把套接字地址族成员设置为AF_UNSPEC。


Raw socket(原始套接字) 它和其他的套接字的不同之处在于它工作在网络层或数据链路层,而其他类型的套接字工作在传输层,只能进行传输层数据操作。

p32:

1、ACK中的确认号是发送这个ACK的一端所期待的下一个序列号,SYN和FIN都会占据一个字节的序列号空间。

client: socket->connect; server: socket->bind-> listen -> accept;

2、建立TCP链接就好比一个电话系统:

socket:有电话可用,

#include <sys/socket.h> int socket(int family, int type, int protocol); return sockfd, else -1;

bind:告诉别人你的电话号码,这样他们可以称呼你;

#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); return 0, else -1;

bind的作用就是绑定端口,如果不调用bind,则后面调用listen内核会自动选取一个临时端口,然而作为server,需要一个公开端口。

可以指定ip地址,对于client,这就指定了发送包的源地址,对于server,只接受目的地址为此地址的包。

listen:打开电话振铃,这样当有一个外来呼叫到达时,你就可以听到,仅由服务器端设定。

#include<sys/socket.h> int listen(int sockfd, int backlog); return 0 else -1;

内核为任何一个给定的监听套接字维护两个队列:

1、未完成链接队列,每个SYN分节对应其中一项。处于SYN_RCVD;

2、已完成链接队列,处于ESTABLISHED状态。

两个队列之和不超过backlog。

connect:要求我们知道对方的电话号码并拨打他。

#include <sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

对tcp来说,connect发起三次握手。

accept:发生在被呼叫的人应答电话之时,由accept返回的客户的标识(即客户的IP地址和端口号)

类似于让电话机的呼叫者ID功能部件显示呼叫者的电话号码,

accept只在链接建立之后返回客户的标识。

#include<sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); return connfd, else -1;

DNS:提供一种类似于电话薄的服务,

getaddrinfo:类似于在电话薄中查找某个人的电话号码

getnameinfo:类似于有一本按照电话号码而不是按照用户名排序的电话薄。


p37

主动关闭的那端经历TIME_WAIT状态:这个状态持续时间是最长分节生命期(MSL)的两倍。

存在理由:

1)可靠的实现TCP全双工链接的终止;(ACK丢失)

2)允许老的重复分节在网络中消逝;(防止第二次同样的链接收到上次链接的数据)


p94

#include<sys/socket.h>

#include getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);

返回本地IP地址和本地端口;

#include getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

可用于获取某个套接字的地址族。

书中举得例子:inetd fork并exec某个TCP服务器程序,

如何将connfd传递给exec的程序呢?

1、将connfd变为字符串,作为命令行参数传递;

2、约定在 调用exec之前,总是把某个特定描述符置为所接受的已连接套接字的描述符,

例如,inetd采用0、1、2描述符。


#include <unistd.h> pid_t fork(void); return 0 or pid else -1;

fork之后,父子进程只共享描述符;

fork有两个典型的用法;

1、一个进程创建一个自身的副本,每个副本干各自的事情。这是网络服务器的典型用法;

2、一个进程想要执行另一个程序,先创建副本,然后其中一个副本调用exec,把自身替换成新程序。

常见有6个exec函数(最终都是调用execve系统调用),他们之间的主要区别是:

1、待执行的程序文件由文件名(filename)还是路径名(pathname)指定;

2、新程序的参数是一一列出还是由一个指针数组来引用;

3、把调用进程的环境传递给想新进程还是给新进程 指定新的环境;



#include <sys/socket.h> int shutdown(int sockfd, int howto);

由close可以终止网络连接,但是close有两个限制:

1、close把描述符减一,仅在该计数变为0时才关闭套接字,我们用shutdown可以直接激发TCP的正常终止序列;

2、close是终止读和写两个方向上的数据传送,而shutdown可以选择(howto参数);


p103

信号(signal)是告知某个进程发生了某个事件的通知,也称为软件中断(software interrupt),通常是异步的,

也就是在进程预先不知道信号的准确发生时刻。

信号可以由一个进程发给另一个进程或由内核发给某个进程。

可以调用sigaction函数(POSIX方法)来设定一个信号的处置,有三个选择:

1、提供一个信号处理函数(signal handler),捕获(catching)信号,有两个信号不能被捕获(SIGKILL, SIGSTOP)

void handler(int signo);

2、忽略(ignore)某一个信号,有两个信号不能被忽略(SIGKILL, SIGSTOP);

3、设置为SIG_DFL来启动它的默认(default)处置。

有些信号的默认行为是终止进程,还有些就是忽略掉。


p107:

处理被中断的系统调用:

慢系统调用(slow system call): read accpet等网络函数,还有对管道和终端设备的读和写

当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTER错误

有些系统会自动重启系统调用,有些不会,所以需要我们自己重启

for(;;){    clien = sizeof(cliaddr);    if( (connfd = accept(listenfd, (SA*)&cliaddr, &clilne)) < 0){        if(errno == EINTER)            continue;        else             err_sys("accept error");    }}

对accept read write select open等函数可以重启,但是对connect就不可以调用,否则会返回错误(EADDRINUSE),这时必须调用select来等待连接完成,

当select成功或失败时返回。


p108:

问题1:僵死子进程

wait和waitid用来处理已终止的子进程

#include<sys/wait.h> pid_t wait(int *statloc);  pid_t waitpid(pid_t pid, int *statloc, int options); return pid else 0 or -1;

信号时不排队的,所以有多个信号时,应该用waitpid以免留下僵死进程

void sig_chid(int signo){    pid_t pid;    int stat;    // WNOHANG:告知waitpid在尚未有终止的子进程时不要阻塞    // -1: 表示等待第一个终止的子进程    while((pid = waitpid(-1, &stat, WNOHANG)) > 0){        printf("child %d terminated\n",pid);    }}

结论:

1、在fork子进程的时候,必须捕获SIGCHLD信号;

2、当捕获信号时,必须处理被中断的系统调用;

3、SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵尸进程。


问题2:当服务器进程终止时,客户进程没有被告知;

当服务器终止时,会向客户进程发送FIN,但是由于客户进程正阻塞于等待用户输入而未接收到该通知,

可以用select或poll函数来处理这种情形。


问题3:服务器主机崩溃的情形要等到客户想服务器发送了数据才能检测到,如何快速检测?

可以利用SO_KEEPALIVE套接字选项来解决该问题(7.5)。


问题4:当客户和服务器之间传递数值数据时,字节序和所支持的长整数的大小不一致

解决方案:

1、把所有的数值数据当做文本串来传递。前提假设客户和服务器主机有相同的字符集;

2、显示定义所支持数据类型的二进制格式(位数,字节序)。RPC就是用这种方法。


p122

I/O复用是一种让进程预先告知内核能力,使得内核一旦发现进程预先告知时指定的一个或多个I/O条件(就是描述符)就绪(可以读/写了),内核就通知进程。

linux有4个调用可实现I/O复用:select、poll继承自Unix系统。pselect是select到Posix版。epoll是linux2.6内核特有的。

网络应用场合:

1)当客户处理多个描述符(通常为交互式输入和网络套接字);

2)一个客户同时处理多个套接字;

3)如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字;

4)如果一个服务器既要处理TCP,又要处理UDP;

5)如果一个服务器要处理多个服务或者多个协议(inetd);


p127

select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

select会有最大描述符数的限制。

#include <sys/select.h> #include <sys/time.h>

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); return count of descriptor or 0 for timeout or -1 for error.

struct timeval{

long tv_sec; /* seconds*/

lont tv_usec; /* microseconds*/

};

void FD_ZERO(fd_set *fdset);

void FD_SET(int fd, fd_set *fdset);

void FD_CLR(int fd, fd_set *fdset);

int  FD_ISSET(int fd, fd_set *fdset);

p130

关于描述符就绪条件,可读吗?可写吗?异常吗?等等

套接字可读:

1)接收缓冲区的可读数据大于低水位标记,可用SO_RCVLOWAT套接字选项设置;

2)接收到FIN信号,读的话不阻塞,返回0;

3)监听套接字,已完成连接数不为0,则accept后不阻塞。

4)套接字上有错误待处理,读则不阻塞并返回-1;

套接字可写:

1)发送缓冲区的可用空间大于或等于低水位标记,可用SO_SNDLOWAT套接字选项设置;

2)套接字写半部关闭,如果写的话,产生SIGPIPE信号;

3)使用非阻塞connect的套接字链接,或connect已经失败告终;

4)如果有套接字错误待处理,对其写将不阻塞并返回-1;


p142

服务器易受拒绝服务攻击(DOS),可能的解决办法包括:

1)使用非阻塞式I/O(第16章)

2)让每个客户由单独的控制线程提供服务(例如为每个客户创建一个子进程或线程)

3)对I/O操作设置一个超时。


p144

poll 函数最初局限于流设备,后来取消限制,它提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息,它不用担心最大描述符数目的限制。

#include<poll.h> int poll(strcut polld* fdarray, unsigned long nfds, int timeout)

struct pollfd{

int fd;

short events;

short revents;

}

有关select、pselect、poll这块看的不是很清楚。

第六章看的不是很好。


第七章:套接字选项

有很多方法来获取和设置影响套接字的选项:

1、getsockopt和setsockopt函数

2、fcntl函数

3、ioctl函数

套接字选项大约可分为两大基本类型:一是启用或禁止某个特性的二元选项;二是取得并返回我们可以设置或检查的特定值的选项。

最常用的选项是1:SO_KEEPALIVE; 2:SO_RCVBUF; 3:SO_SNDBUF; 4:SO_REUSEADDR; 5:SO_LINGER;

1:SO_KEEPALIVE,如果设置这个选项,如果2h内该套接字的任意方向上没有数据交换,TCP就自动给对端发送keep-alive probe.对端必须响应,

会导致以下三种情况之一:

1)对端以期望的ACK响应。应用程序得不到通知;

2)对端以RST响应,他告知本端TCP,对端已崩溃且已重启。ECONNRESET错误,套接字关闭;

3)对端没有任何响应。ETIMEOUT错误,套接字关闭;

此选项一般是服务器段使用,检测半开链接并终止他们。


4: SO_REUSEADDR 一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用,即没有time_wait状态了。

这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。


5:SO_LINGER,指定close函数对面向链接的协议如何操作,默认操作室close立即返回,



第12章:IPv4 与 IPv6 的互操作性

本章假设主机都运行着双栈(dual stacks),指IPv4和IPv6协议栈。

IPv4客户与IPv6服务器进行通信的步骤总结如下:

1)IPv6服务器启动后创建一个IPv6的监听套接字,我们假设服务器把统配地址捆绑到该套接字;

2)IPv4客户调用gethostbyname找到服务器主机的一个A记录;

3)客户调用connect,导致客户主机发送一个IPv4 SYN到服务器主机;

4)服务器主机接收这个目的地址为IPv6监听套接字的IPv4 SYN,设置一个标志指示本链接应使用IPv4映射的IPv6地址,然后相应一个IPv4 SYN/ACK。该链接建立后,有accept返回给服务器的地址就是这个IPv4映射的IPv6地址。

5)当服务器主机往这个IPv4映射的IPv6地址发送TCP分节时,其IP栈产生目的地址为IPv4地址的IPv4载送数据报。因此,客户和服务器之间的所有通信都使用IPv4的载送数据报。

6)除非服务器显示检查这个IPv6地址是不是IPv4映射的IPv6地址,否则它永远也不知道是在和IPv6服务器通信。

上述的前提假设是双栈服务器既有一个IPv4地址,也有一个IPv6地址。


IPv6客户与IPv4服务器进行通信的步骤总结如下:

1)一个IPv4服务器在只支持IPv4的主机上启动后创建一个IPv4的监听套接字;

2)IPv6客户启动后调用getadrinfo单纯查找IPv6地址。既然只支持IPv4的那个服务器主机只有A记录,那么给客户的是一个IPv4映射的IPv6地址;

3)IPv6客户在作为函数参数的IPv6套接字地址结构中设置这个IPv4映射的IPv6地址后调用connect。内核检测到这个映射后 自动发送一个IPv4 SYN到服务器。

4)服务器相应一个IPv4 SYN/ACK,链接于是通过IPv4数据报建立。


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

p286

守护进程(daemon)是在后台运行且不与任何控制终端关联的进程。通常由系统初始化脚本启动。当然也可以从某个终端由用户在shell提示符下键入命令行,这样的守护进程必须亲自脱离于控制终端的关联,从而避免与作业控制、终端会话管理、终端产生信号等发生任何不期望的交互,也可避免在后台运行的守护进程非预期の输出到终端,syslogd守护进程接受来自守护进程的消息。

守护进程有很多启动方法:

1)在系统启动阶段由系统初始化脚本启动,例如inetd超级服务器、Web服务器、邮件服务器(sendmail)、syslogd守护进程、cron守护进程;

2)许多网络服务器由inetd启动,inetd监听网络请求(Telnet、FTP等),每当有一个请求时,启动相应的服务器;

3)cron守护进程会定期执行一些程序;

4)at命令用于指定将来某个时刻程序执行,当来时,由cron守护进程启动;

5)还可以由用户终端在前台或后台启动,是为了测试守护程序或重启守护进程。


p289

由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。

 将普通进程转换成守护进程(在linux有daemon函数)

掌握创建顺序

Linux守护进程(http://blog.chinaunix.net/uid-21411227-id-1826736.html)

1)创建子进程,父进程推出:子进程会成为孤儿进程,由init进程收养;

2)在子进程中创建新会话:由系统函数setsid完成,setsid函数作用:
  setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有下面的3个作用:
  (1)让进程摆脱原会话的控制
  (2)让进程摆脱原进程组的控制
  (3)让进程摆脱原控制终端的控制

在创建守护进程时为什么要调用setsid函数呢?由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,还还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。

进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因组长进程的退出而受到影响。
会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

3)改变当前目录为根目录:由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入用户模式)。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数式chdir。

4)重设文件权限掩码:把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

5)关闭文件描述符:同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

这样,一个简单的守护进程就建立起来了。

例子:

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<fcntl.h>#include<sys/types.h>#include<unistd.h>#include<sys/wait.h>#define MAXFILE 65535int main(){  pid_t pc;  int i,fd,len;  char *buf="this is a Dameon\n";  len = strlen(buf);  pc = fork(); //第一步    if(pc<0){      printf("error fork\n");      exit(1);  }else if(PC>0)  exit(0);  setsid(); //第二步  chdir("/"); //第三步  umask(0); //第四步  for(i=0;i<MAXFILE;i++) //第五步    close(i);    //以下的daemon开始工作了  while(1){      if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0){          perror("open");          exit(1);      }      write(fd,buf,len+1);      close(fd);      sleep(10);    }}

inetd守护进程

1)通过由inetd处理普通守护进程的大部分启动细节以简化守护进程的编写。这么一来每个服务器不再有调用deamon_init函数的必要;

2)单个进程(inetd)就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法,这么做减少系统总进程数。


p524

针对一个套接字使用信号驱动式I/O(SIGIO)要求进程执行以下3个步骤:

1)建立SIGIO信号的信号处理函数;

2)设置该套接字的属主,通常使用fcntl的F_SETOWN命令设置;

3)开启该套接字的信号驱动式I/O,通常通过使用fcntl的F_SETFL命令打开O_ASYNC标志完成;

不幸的是,信号驱动式I/O对于TCP套接字近乎无用,问题在于该套接字产生得过于频繁,并且它的出现没有告诉我们发生了什么。

唯一实例:基于UDP的NTP服务器程序,是为了时间精确。


p534

同一进程内的所有线程共享:

1)全局变量;2)进程指令;3)大多数数据;4)打开的文件(文件描述符);

5)信号处理函数和信号处置;6)当前的工作目录;7)用户ID和组ID;

每个线程各自的:

1)线程ID;2)寄存器集合,包括pc和栈指针;3)栈(用于存放局部变量和返回地址);

4)错误符(errno);5)信号掩码(umask);6)优先级;

线程终止的方法:

1)调用pthread_exit(void *status);

2)通过return;

3)如果进程的main函数或任何线程调用了exit,整个进程就终止(任意线程也是);

感觉有必要把这些过程好好的弄清楚。


p542

线程特定数据

暂时没看



p552

互斥锁(mutex, mutual exclusion)

#include<pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mptr);

int pthread_mutex_unlock(pthread_mutex_t *mptr);


条件变量:提供信号机制,条件变量被用来进行线承间的同步,弥补了互斥锁的不足,它常和互斥锁一起使用。

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); 

该函数把调用线程投入睡眠并释放调用线程持有的互斥锁。当返回时,该线程在此持有该互斥锁。

int pthread_cond_signal(pthread_cond_t *cptr);  唤醒等在相应条件变量上的单个线程。


第30章:客户/服务器程序设计范式【这个还没总结】

WebStone、Webbench:服务器性能基准程序;

1)TCP迭代服务器程序;

2)TCP并发服务器程序,每个客户一个子进程;

DNS轮询:大多域名注册商都支持多条A记录的解析,其实这就是DNS轮询,DNS服务器将解析请求按照A记录的顺序,逐一分配到不同的IP上,这样就完成了简单的负载均衡。

int main(){       int listenfd, connfd;    pid_t childpid;    void sig_chld(int), sig_int(int), web_child(int);    socklen_t clilen, addrlen;    struct sockaddr* cliaddr;    if(argc == 2)        listenfd = Tcp_listen(NULL, argv[1], &addrlen);    else if(argc == 3)        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);    else        err_quit("usage:serv01 [<host>] <port#>");    cliaddr = Malloc(addrlen);        Signal(SIGCHLD, sig_chld);    Signal(SIGINT, sig_int);        for(;;){        clilen = addrlen;        if( (connfd = accept(listenfd, cliaddr, &clilen)) < 0){            if(errno == EINTR)                continue;            else                err_sys("accept error");        }        if( (childpid = Fork()) == 0){ // child process            Close(listenfd); // close listening socket            web_child(connfd); // process request            exit(0);        }        Close(connfd);  //parent closes connected socket;    }    return 0;}

 long atol(const char *nptr);将字符串变为long正数。

3)预先派生子进程。

(1)如果有多个进程阻塞在引用同一实体(例如socket套接字或普通文件,由file借个直接或间接描述)的描述符上,那么最好直接阻塞在accept上而不是select(容易发生冲突);

在accept上也会发生错误,解决方法是在调用accept前后安置某种形式的锁(lock),例如以fcntl函数呈现的POSIX文件上锁功能。

(2)线程上锁保护accept,不仅适用于同一进程内各线程之间的上锁,而且适用于不同进程之间的上锁。

不同进程之间使用线程上锁要求:1:互斥锁变量必须存放在共享内存中;2:必须告知线程函数库这是在不同进程之间共享的互斥锁。

(3)传递描述符,只让父进程调用accept,然后让所接受的已连接套接字“传递给”某个子进程,绕过了为所有子进程的accept调用提供上锁保护的可能需求,但是父进程必须跟踪子进程的忙闲状态,以便给空闲的子进程传递新的套接字。通过字节流管道(一对Unix域字节流套接字)来实现。

4)TCP并发服务器程序,每个客户一个线程。

5)TCP预先穿件服务器程序,每个线程各自accept,我们可以用互斥锁来保证每个时刻只有一个线程在调用accept。

6)TCP预先创建线程服务器程序,主线程统一accept,既然每个线程可以共享内存,那么只需用线程锁和条件变量就可以轻松实现啦。(这个比较赞!!!)

在第三部分,今天(2014年8月16日)大体看了下,还需要好好看。


这个我大致看了下,下面还要继续看,加油。



%%%%%%%%%%%%%%%%%%%%%%《TCP/IP高效编程》%%%%%%%%%%%%%%%%%%%

p45

对于读取socket定长数据的readn应该这么写

int readn(SOCKET fd, char* bp, size_t len){    int cnt;    int rc;    cnt = len;    while(cnt > 0){        rc = recv(fd, bp, cnt, 0);        if(rc < 0){            if(errno == EINTR) // interupeted                continue;            return -1;        }        if(rc == 0)            return len-cnt;        bp += rc;        cnt -= rc;    }    return len;}






0 0
原创粉丝点击