收到网易的入职前作业,感觉都费劲(捂脸)。学习之余做点笔记吧。
这部分主要涉及服务器端的作业:用Python完成一个聊天窗口,以及若干附加的功能。
在看python文档时候注意到了两个地址簇或者叫接口(因为直接影响了通讯协议):AF_INET和AF_UNIX。查阅到资料Socket通信中AF_INET 和 AF_UNIX域的区别,最关键的一点:AF_INET不仅可以用作本机的跨进程通信,同样的可以用于不同机器之间的通信,其就是为了在不同机器之间进行网络互联传递数据而生。而AF_UNIX则只能用于本机内进程之间的通信。
SCTP协议与TCP和UDP协议同样位于传输层,在客户和服务器之间提供关联,并保证可靠、排序、流量控制以及全双工的数据传输。关联指代两个系统间的一次通信,可能涉及不止两个地址。
SCTP能在连接的端点之间提供多个流,每个流各自可靠按序递送消息。与TCP不同,一个流上某个消息的丢失不会阻塞其他流的消息投递,而TCP需要等待该丢失的数据被修复,否则一直被阻塞。SCTP还支持单个端点拥有多个IP地址。在建立连接后,如果某个IP通路之间发生故障,协议就可以切换到该端点的另一个地址来规避故障。
SCTP在建立连接时,采用四路握手的方式进行。简单来说,在客户端第一次发送连接建立请求后,服务器会返回一个带有cookie的应答。随后客户端再附带上cookie发送一次请求,在服务器相应后,则成功建立连接。四路握手结束后,两端各选择一个主目的地址用作数据发送到的目的地。
SCTP在断开连接时没有类似于TCP的TIME_WAIT状态,因为SCTP使用了验证标记cookie。因此,新/旧连接之间可以通过该标记来进行区分。
套接字地址结构
POSIX中对IPv4地址结构定义在<netinet/in.h>
中,具体如下:
struct in_addr {
in_addr_t s_addr //通常是32位无符号整数,网络字节序地址
}
struct sockaddr_in{
uint8_t sin_len;
sa_family_t sin_family; //8或16位无符号整数,表示地址结构的地址族,IPv4SHI AF_INET,IPv6是AF_INET6
in_port_t sin_port; //一般为uint16_t,即16位无符号整数
struct in_addr sin_addr;
char sin_zero[8];
}
POSIX规范实际**只需要这个结构中的3个字段:s_addr
、sin_family
、sin_port
。
其中有趣的一点,
in_addr
是一个结构体,即便它内部只有一个整数,这是有历史原因的。早在地址被划分成A、B、C三类的时期,in_addr
被定义为多种结构的联合,从而允许用户访问地址中的每4个字节,便于或许所需的地址。后来CIDR的出现,这种划分方式淡出了历史舞台,但这种结构保留了下来。
不同套接字的地址结构
图中最右侧的存储是指作为IPv6套接字API一部分而定义的新的通用套接字。它克服了sockaddr
的一些缺点,用以取代sockaddr
,定义在<netinet/in.h>
中。
IPv4地址中的长度字段是随着4.3BSD Reno版本增加的。要是长度字段在最原始的套接字API中就提供了,那么套接字函数就不在需要长度参数——例如
bind
、connect
函数的第三个参数。结构的大小可以包含在结构的长度字段中(有点类似于STL对string的处理方式)
值-结果参数
1.从进程到内核传递套接字地址结构的函数有3个:bind
、connect
和sendto
。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小
2.从内核到进程传递套接字地址结构的函数有4个:accept
,recvfrom
,getsockname
和getpeername
。这四个函数的其中两个参数是指向某个套接字地址结构的指针和指向该结构大小的整数变量的指针。
把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果,它告诉内核在该结构中究竟存储了多少信息。
如果套接字地址结构是固定长度的(前一张图)如sockaddr_in
,则从内核返回的值总是对应的固定长度。但对于sockaddr_un
这类长度可变的套接字地址结构,返回值可能与传入的值不同。
字节排序和转换函数
写在前面:“大端”和“小端”的大致意思很多人都懂,但具体大端是高位在内存高地址还是低地址经常让人混淆。《Unix网络编程》书中有一个解释,我觉得比较清晰易理解:
“小端”和“大端”表示多个字节值的哪一端(大或小)存储在该值的起始地址。
值的大端小端,按照字面理解,显然“大端”表示高位,“小端”表示低位。内存中的起始地址肯定是指内存中地址较低的位。
网络协议使用大端字节序来传送多字节整数,如16位的端口号和32位的IPv4地址。字节序转换有以下4个函数:
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);
函数名中,h代表host,n代表network,s代表short,l代表long。这些命名是4.2BSD使其的产物。如今即使长整形占64位,htonl
和ntohl
操作的仍是32位的值。
此外有几个地址转换函数,将地址在习惯的点分十进制字符串与32位网络字节序二进制数值(针对IPv4)之间转换。
随着IPv6出现又天剑了几个新的函数,并具有对IPv4的兼容性,因此推荐使用。具体如下:
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
其中p和n分别代表表达(presentation)和数值(numeric)。两个函数中family
既可以是AF_INET
也可以是AF_INET6
。inet_pton()
将字符串式的地址转换为二进制结果存储于*addrptr
中,如果成功返回1,否则返回0。inet_ntop()
进行相反的转换,len
帮助制定目标存储空间的大小避免溢出。如果len
太小,则**会返回一个空指针,并置errno
为ENOSPC
。
close()函数
close
一个TCP套接字的默认行为是把该套接字标记为已关闭,然后立刻返回调用进程。该套接字描述符不能再由调用进程使用。也就是不能再在该套接字上调用read
和write
。(不过TCP协议仍将发送已排队等待发送到对端的数据),然后考虑关闭连接。这里说考虑是因为,文件或套接字是采用引用计数的方式进行管理。它是当前打开着的引用该文件或套接字的描述符的个数。例如,当服务器接到一个请求时,可以采用如下处理方式:
for(; ; ){
connfd = accept(listenfd, ...);
if( (pid = fork() )== 0){
close(listenfd); //在子进程中关闭 监听套接字
do_something(connfd);
close(connfd); //可省略,因为exit()会关闭所有描述符
exit(0);
}
close(connfd); //在父进程中关闭 已连接套接字
}
这段代码在父进程和子进程中分别关闭了一个自己不需要的套接字描述符,但不会立刻引起TCP的私分组连接终止序列,这是因为该描述符还被其他进程占用着,没有减少到0。
如果我们确实想在某个TCP连接上发送一个FIN,那么可以使用shutdown()
函数代替close
。
另外还得注意,(在上述父子进程的模式中)父进程必须对每个由accept()
返回的已连接套接字都调用close()
,否则,父进程终将耗尽所有的可用描述符(因为每个进程可用的打开着的描述符是有限的),更重要的是,这些客户连接都不会被终止,因为父进程一直打开着对应的已连接套接字。
POSIX 信号处理
信号有时也称为软中断。信号可以:
- 由一个进程发给另一个进程(或自身)
- 由内核发给某个进程
正常程序中,由父进程派生的子进程完成退出后,不会自动消失,而是向父进程发送一个SIGCHLD信号。该信号的默认处理是被忽略,这时子进程会进入僵死状态,从而导致系统资源无法回收被浪费。
我们可以提供信号处理函数来处理捕获到的信号(除了SIGKILL
和SIGSTOP
),一般采用调用sigaction()
或signal()
函数来设置信号处理函数。
POSIX上的信号处理有以下特点:
- 一旦安装了信号处理函数,就一直安装着(这里我记得,仅限于进程内部,不同进程/重启进程时恢复系统默认状态)
- 在一个信号处理函数运行期间,正被递交的信号(即:同一个类型的信号)是阻塞的,而且,调用
sigaction
函数中的sa_mask
参数制定的任何额外信号也被阻塞(除非把sa_mask
置为空,则只有被捕获的信号被阻塞) - 如果一个信号在被阻塞期间产生了一次或多次,那么该信号在被解除阻塞后只递交一次。
另外,值得注意的是,在accept
(慢系统调用——指可能永远阻塞的系统调用)期间捕获信号并转到信号处理函数。该函数退出后,内核就会使accept
返回一个EINTR
错误(被中断的系统调用),因此父进程需要处理这项错误。
wait和waitpid函数
参考链接:http://blog.youkuaiyun.com/xilihong816/article/details/52900101
我们可以调用wait
或waitpid
函数来处理前文提到的,已终止的子进程:
pid_t wait(int *statlc);
pid_t waitpid(pid_t pid, int *statloc, int options);
这两个函数均返回两个值:已终止的子进程PID,和通过statloc
指针(如果不为NULL
)返回的进程终止状态(整数)。我们可以调用特定的宏来检查终止状态。
WIFEXITED(status)
这个宏用来指出子进程是否为正常退出的WEXITSTATUS(status)
当WIFEXITED
返回非零值时,我们可以用这个宏来提取子进程的返回值
wait
处理第一个已终止的子进程,如果没有,但有至少一个子进程仍在运行,则阻塞等待到有一个子进程终止为止。
waitpid
除了与wait
相同的部分外,可以用PID指定子进程ID。PID有以下取值方式:
pid>0
时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid
就会一直等下去。pid=-1
时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。pid=0
时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。pid<-1
时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
另外,options
提供了一些额外的选项来控制waitpid
,目前在Linux中只支持WNOHANG
和WUNTRACED
两个选项,这是两个常数,可以用|
运算符把它们连接起来使用。尤其是WNOHANG
这个选项,可以避免wait
方法因没有完成的子进程而阻塞回收进程(一般是父进程调用)
返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
1、当正常返回的时候,waitpid返回收集到的子进程的进程ID;
2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
PS 当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
SIGPIPE信号
在客户端与服务器建立连接后,如果服务器单方面主动关闭连接(比如终止进程),这时服务器会对客户端发送一个FIN
,连接进入半关闭状态(两次握手)。这时客户端仍可以向该连接的套接字中写入数据(这时允许的)。当服务器接收到来自客户的数据时,如果服务器打开套接字的进程已经终止(不可能继续接受数据了),于是相应一个RST
信号。要是客户端不理会这个RST
,继续写入数据时。也就是说当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE
信号,信号默认行为是终止进程。另外,无论怎么处理该信号,写操作都会返回一个EPIPE
错误。
如何在第一次写操作时而不是第二次写操作时捕获该信号?这是不可能的。第一次写操作引发RST,第二次写引发SIGPIPE。写一个已接受FIN的套接字不成问题,但写一个接受了RST的套接字则是一个错误。