《UNIX网络编程》读书笔记

《UNIX网络编程》是一本关于UNIX系统和类UNIX系统网络编程方面的书,详尽地介绍了关于网络API的调用以及相关的系统调用方面的知识,还有不少关于C/S架构设计的讨论,如果想了解学习socket方面的知识,这本书是不二之选。

基础部分

动态链接库

1
2
3
4
5
6
7
8
#生成so文件
hui@linux:~/test$ gcc hello.c -fPIC -shared -o libhello.so
#移动到库目录
hui@linux:~ mv libhello.so /usr/lib
#链接库文件
hui@linux:~ gcc -o test test.c -L. -lhello
#运行
hui@linux:~ ./test

动态加载库

动态加载库可用程序加载的方式控制什么时候加载链接库

  • dlopen(),打开动态链接库。flag为打开方式,一般为RTLD_LASY

    void dlopen(const char filename,int flag )

  • dlsym(),获得函数打针。

    void dlsym(void handle, char* symbol)

进程的产生方式

  • fork(),以复制方式产生,除内存等与父进程不同,其它与父进程一样。

  • system(),调用shell的外部命令在当前进程开始另外一个进程,执行特定命令时阻塞当前进程,直到其执行完成。它会调用fork、execve和waitpid等函数,其中
    任意整一个调用失败将导致system()调用失败。

  • exec族函数(execl()、execlp()、execle()、execv()和execvp()),产生的新进程会替代原来的进程,其PID与原来的进程相同,只有execve()是真正的系统调用。

数据链路层

  • 数据帧里,目的地址和源地址各占6字节(MAC地址)

  • 数据段的大小为46~1500字节,1500为MTU(最大传输单元)

  • 数据帧尾部为校验和,采用CRC16的校验方式

网络层

  • IP数据报的头部最短为20字节,最长为60字节,以字(32位)为增量变化

  • 总长度字段为16位,所以IP数据段最大可达65536字节

  • TTL(Time To Live),生存时间字段表示数据报文最多可以经过的路由数量。每经过一个路由TTL的值减1,当为0时,路由器丢弃此包

协议报文

TCP

  • 头部为20个字节,其中源端口号和目的端口号各占2字节

  • 序列号长度为32位,表示分配给TCP包的编号。连接成功后生成一个ISN(Initial Sequence Number),此后按字节的大小进行递增

UDP

  • UDP的校验和字段是可选的,不像TCP那样,可以不进行CRC校验

  • UDP协议比TCP协议执行的速度快得多

ARP,地址解析协议

  • ARP协议为IP地址到硬件地址提供动态的映射关系

  • ARP缓存的高速缓存维持这种映射关系,存放着最近IP到硬件地址的映射记录,bash命令arp -a,查看ARP高速缓存

  • ARP的实现方式在以太网上做广播,查询目的IP,当数据帧头部的目的地址全为1时为广播帧

套接字

查看文件状态

查看文件的所有者,文件的修改时间和文件的大小等,通过函数把文件信息保存到结构stat里。

  • int stat(const char path, struct stat buf)

  • int fstat(int filedes, struct stat *buf)

  • int lstat(const char path, struct stat buf)

套接字描述符判定函数issockettype()

1
2
int s = socket(AF_INET, SOCK_STREAM, 0);
int ret = issockettype(s); //返回1,说明是套接字

查看主机信息

通过gethostbyname()和gethostbyaddr()函数可获取主机信息,并保存在结构体hostent里。由于非线程安全,建议使用替代函数getaddrinfo和getnameinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <netdb.h>
#include <stdio.h>
#include <string.h>
...
struct hostent *ht = NULL;
ht = gethostbyname(host); //为不可重入函数,返回值后应立即将结果取出
if(ht)
{
int i;
for(i=0;;i++)
{ //通过一个循环查看主机的别名
if(ht->h_aliases[i]!=NULL)
printf("alis[%d], %s\n",i,ht->h_aliases[i]);
else
break;
}
}

IO模型:

阻塞、非阻塞(fcntl)、IO复用(select和pselect、poll和ppoll)、信号驱动(sigaction)和异步(aio_read)。

Linux平台下内核调度的精度为10毫秒级。

UDP乱序

原因

  • 路由器存储转发造成的顺序更改
  • 路由器路径不同造成的顺序更改

解决办法 发送端在数据段加入数据报序号,这样接收端对接收到的数据进行简单的处理就可以重新获取到原始顺序的数据。

UDP中使用connect()函数

仅仅表示确定了另一方的地址,没有别的作用

  • 发送操作不能再使用sendto(),要使用write(),直接使用套接字文件描述符,不再指定目的地址和端口号

  • 接收操作不能再使用recvfrom(),要使用read()

  • 多次使用会改变原来的目的地址和端口号

Unix域套接字

Unix域套接字有字节流套接字和数据报套接字。前者类似于TCP,后者类似于UDP

  • 与TCP套接字相比,在同一台主机的传输速度是后者的两倍

  • Unix域套接字可以在同一台主机的各进程之间传递描述符

  • Unix域套接字与传统套接字的区别是用路径名来表示协议族的描述

  • Unix域字节流套接字的connect() 函数发现监听的队列满了返回ECONNREFUSED错误,TCP会忽略到来的SYS

广播、多播

多播

优点

  • 具有共同业务的主机加入同一数据流,共享同一通道

  • 服务器的总带宽不受客户端带宽的限制

  • 与单播一样,多播是允许在广域网即Internet上进行传输的,而广播只能在同一局域网进行

缺点

  • 与单播相比没有纠错机制,但可在应用层实现

  • 网络支持存在缺陷,需要路由器及网络协议栈的支持

类别

  • 局部多播地址,在224.0.0.0~224.0.0.255之间,这是为路由协议和其他用途保留的地址,路由器不转发属于此范围的IP包

  • 预留多播地址,224.0.1.0~238.255.255.255之间,可用于全球范围或网络协议

  • 管理权限多播地址,在239.0.0.0~239.255.255.255之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围

原始套接字

套接字选项(SOL_SOCKET)

在进行网络编程的时候,经常需要查看或者设置套接字的某些特性,如地址复用、读写数据的超时时间、对读缓冲区的大小调整等操作,可通过setsockopt()和getsockopt()函数进行相关设置和查看。SOL_SOCKET为通用类型的套接字选项。

SO_KEEPALIVE

使用场景主要是可能发送长时间无数据响应的TCP连接。设置后,TCP会自动发送报文,对方必须进行回应。有三种情况:

  • TCP连接正常,发送一个ACK响应,这个过程应用层是不知道的

  • 对方发送RST响应,对方在2个小时内进行了重启或者崩溃

  • 如果对方没有任何响应,则本机会发送另外8个活动探测报文,第一个报文无响应,发生ETIMEOUT错误;收到ICMP报文响应,主机不可达,发生EHOSTUNERACH错误

1
2
3
int optval = 1; //设置有效
int s = socket(AF_INET, SOCK_STREAM, 0);
int err = setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));

SO_LINGER

直接调用close函数关闭连接,缓冲区剩余数据的处理是不可知的,设置SO_LINGER可阻塞close函数,直到剩余数据全部发送给对方,保证了TCP连接两端的正常关闭。

1
2
3
4
struct linger optval; //通过此结构进行SO_LINGER操作
optval.l_onoff = 1;
optval.l_linger = 60; //设置超时时间
err = setsockopt(s, SOL_SOCKET, SO_LINGER, &optval, sizeof(optval));

optval.l_onoff值为0时,使用系统默认的关闭行为,对剩余数据的处理情况未知;其值为1时,在设定的超时时间内发送数据,发送成功返回0,发送失败返回-1;其值为1,l_linger为0,表示立刻关闭,调用close函数立即返回,缓冲区数据被丢弃。

SO_OOBINLINE

有时,发送的数据可能会超过所限制的数据量,可设置该选项接收带外数据,带外数据和一般数据一起接收,相当于增加了带宽。

SO_RECBUF和 SO_SNDBUF

设置发送和接收缓冲区的大小。UDP连接里,如果接收方缓冲区过小,可能会导致缓冲区溢出,新的数据覆盖旧的数据。

对于TCP,客户端接收缓冲区是在connect()前设置的,服务器接收缓冲区是在listen()前设置的。实际设定值并非用户输入的大小,根据其大小,最终设定值可能为输入值的2倍,也可能为其它值。
SO_RECTIMEO和 SO_SNDTIMEO

设置发送和接收数据的超时时间,通过struct timeval来实现。

SO_REUSEADDR

地址重用,防止服务器因多个程序侦听同一端口发生错误。比如有的服务器程序非正常退出时,占用的某一端口,可能要过一段时间才允许其它程序使用,不设置地址重用将不能使用此端口。这与SO_EXCLUSIVEADDRUSE端口独占相反。

SO_TYPE

获取套接字类型。

1
2
3
4
5
6
int s = socket(AF_INET, SOCK_STREAM, 0);
int type;
int length = 4;
int err = getsockopt(s, SOL_SOCKET, SO_TYPE, &type, &length);
if(type == SOCK_STREAM)
printf("This is a TCP socket!\n");

SO_BINDTODEVICE

将套接字与某个网络设备绑定。

1
2
char ifname[] = "eth0";
err = setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname));

SO_PRIORITY

设置发送报文的优先级,范围为0~6。

套接字选项(IPPROTO_IP和IPPROTO_TCP)

IP_TTL

设置发送报文的生存时间(TTL),一般情况下为64,对于原始套接字为255。

TCP_KEEPALIVE

设置存活探测的时间间隔,在SO_KEEPALIVE开启的情况下才有效。默认情况下时间时隔为7200s,即两个小时进行一次存活探测。

1
2
3
int alive_time = 60;
int s = socket(AF_INET, SOCK_STREAM, 0);
int err = setsockopt(s, IPPROTO_ICP, TCP_KEEPALIVE, &alive_time, sizeof(int));

TCP_MAXRT

最大重传时间,以秒为单位,0表示系统默认值,-1表示永远重传。

TCP_NODELAY和TCP_CORK

针对Nagle算法的关闭而设置的。Nagle算法将将小于最大分段大小(MSS)的分段组成成更大的帧进行发送。

  • 使用TCP_NODELAY时,客户端的请求不会与其他分段合并,会尽快地发送到服务器端,提高了交互性应用程序的响应速度。HTTPD使用了TCP_NODELAY来发送大块数据,用于提高性能。

  • 使用TCP_CORK时,当发送的数据量达到最大时,才一次性发送全部数据,充分利用网络的带宽,提高数据传输的通信性能。

ioctl()函数和fcntl()函数

  • ioctl(),可以用对于套接字的IO操作(如查TCP连接是否有带外数据)、文件请求、网络接口请求、对ARP高速缓存进行操作和发送路由表请求
  • fcntl(),对套接字进行的部分操作可由ioctl()函数取代,其它的作用还有修改套接字非阻塞属性和设置信号的绑定进程等

原始套接字

作用

  • 读写ICMP、IGMP分组。如ping程序和mrouted

  • 读写特殊的IP数据报,内核不处理其数据报的协议字段

  • 通过函数setsockopt()函数设置选项IP_HDRINGCL可以对IP头部进行操作,修改IP数据和IP层之上的各层数据,构造自己的TCP和UDP分段

发送/接收

  • 可以不使用bind()函数,然后使用sendto()和recvfrom()函数时指定IP。使用bind()后才可以使用send()、recv()等其它不需要指定IP的函数

  • IP_RINCL

    • 使用setsockopt()函数设置IP_RINCL后,发送的数据缓冲区指向IP头部第一个字节的头部,则发送的数据包含IP头部和其后的所有数据,这时需要用户自己填写IP头部和计算校验和,
      并对所包含数据进行处理和计算;此时,接收的缓冲区也指向IP头部的第一个字节。
    • 如果没有设置,则缓冲区指向IP数据区域的第一个字节,不需要填写IP头部。
  • UDP和TCP协议的数据不会传给原始套接字接口,这些协议的数据需要通过数据链路层获得。

  • 如果IP以分段形式达到,则所有分段都已接收并重组后才传给原始套接字

  • 内核不能识别的协议、格式等传给原始套接字,所以可用原始套接字定义用户自己的协议格式

洪水攻击

  • ICMP回显攻击:利用原始套接字向目标机发送大量的回显请求或者回显响应数据,由于此数据协议栈默认是必须处理的,
    因此可以对目标机造成影响。

    • 直接洪水攻击,采用多线程方法一次性地发送多个ICMP报文,让目标主机宕机。容易泄露源IP地址。
    • 伪装IP攻击,在直接洪水攻击的基础上将发送方的IP地址用伪装后的IP地址代替。
    • 反射攻击,让一群主机认为目标机在向他们发送ICMP请求,一群主机向目标机发送ICMP应答包。
  • UDP攻击:向目标机服务端口发送UDP报文,由此目标机需要对端口进行处理,如果知道目标机的基本数据格式,
    则可以构建十分有效的代码来对目标机造成很大伤害。

  • SYN攻击:利用TCP连接中的三次握手,在发送了一个SYN原始报文后,目标机需要对发送的报文进行处理并等待超时。

C/S程序设计范式

套接字服务器类型

  • 循环服务器,又叫迭代服务器。

  • 并发服务器

  • IO复用服务器

TCP并发服务器

  • 统一accept(),单客户端单进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while(1)
{
s_c = accept(s_s, (struct sockaddr*)&from, &len);
if(s_c > 0) //若连接成功
{
int pid = fork();
if(pid > 0)
{
close(s_c); //关闭父进程的客户端连接套接字
}
else
{
handle_request(s_c); //在子进程处理连接请求
return 0;
}
}
}
  • 统一accept(),单客户端单线程
1
2
3
4
5
6
7
8
while(1)
{
s_c = accept(s_s, (struct sockaddr*)&from, &len);
if(s_c > 0) //若连接成功
{
int err = pthread_create(&thread, NULL, handle_request, (void*)&s_c);
}
}
  • 各客户端独自accept(),单客户端单线程,使用互斥锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化或使用函数p_pthread_mutex(&mutex, NULL)
static void *handle_request(void *arg)
{
int s_s =*((int*)arg);
...
while(1)
{
...
pthread_mutex_lock(&mutex); //进入互斥区
s_c = accept(s_s, (struct sockaddr*)&from, &len);
pthread_mutex_unlock(&mutex); //离开互斥区
...
}
}
/*主函数*/
...
for(i=0; i<NUM; i++) //建立线程池
{
pthread_create(&thread[i], NULL, handle_request, (void*)&s_s);
}
for(i=0; i<NUM; i++)
pthread_join(thread[i], NULL); //等待线程结束
close(s_s);

IO复用循环服务器

降低了系统切换的不必要的开支,将主要的系统处理能力集中在核心的业务上。这种服务器模型,在系统开始的时候,
建立多个不同工作类型的处理单元。在客户端连接到来的时候,将客户端的连接放到一个状态池中,对所有客户端的连接状态在一个处理单元中进行轮询处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*主函数*/
...
pthread_t thread[2];
pthread_create(thread[0], NULL, handle_connect, (void*)&s_c); //处理客户端连接
pthread_create(thread[1], NULL, handle_request, NULL); //处理客户端请求
for(i=0; i<2; i++)
pthread_join(thread[i], NULL); //等待线程结束
close(s_s);
/*处理客户端连接*/
int connect_host[CLIENTNUM];
int connect_number;
static void* handle_connect(void *arg)
{
...
while(1)
{
int s_c = accept(s_s, (struct sockaddr*)&from, &len);
printf("A client connect from %s!\n", inet_ntoa(from.sin_addr));
/*查找合适的位置,将客户端的文件描述符放入*/
int i;
for(i=0; i<CLIENTNUM; i++)
{
if(connect_host[i] == -1)
{
connect_host[i] = s_c;
connect_number++;
break;
}
}
}
return NULL;
}
/*处理客户端请求*/
static void* handle_request()
{
...
fd_set scanfd; //侦听描述符集合
int maxfd = -1; //最大文件描述符
while(1)
{
FD_ZERO(&scanfd);
for(i=0; i<CLIENTNUM; i++)
{
if(connect_host[i] != -1)
{
FD_SET(connect_host[i], &scanfd); //将文件描述符放入集合
if(connect_host[i] > maxfd) //更新最大文件描述符
maxfd = connect_host[i];
}
}
int ret = select(maxfd+1, &scanfd, NULL, NULL, &timeval_t);
switch(ret)
{
case 0: //超时
break;
case -1//错误发生
break;
default:
if(connect_number <= 0)
break;
for(i=0; i<CLIENTNUM; i++)
{
if(connect_host[i] != -1)
if(FD_ISSET(connect_host[i], &scanfd))
{
/*接收处理数据*/
}
connect_host[i]=-1; //更新文件描述符在状态表中的值
connect_number--;
close(connect_host[i]);
}
break;
}
}
return NULL;
}

地址转换

IPV6

  • 单播地址

    • 全局可聚集单播地址
    • 站点本地地址
    • 链路本地地址

单播地址“::1”和“0:0:0:0:0:0:0:1”称为回环地址,节点向自己发送数据包时采用回环地址。

  • 多播地址

  • 任播地址,发给一个组内的任意设备而不是所有设备

数据包过滤

netfilter

nf_hook_ops构建钩子

nf_hook_ops是netfilter架构中的常用结构

1
2
static struct nf_hook_ops show_ops = ({NULL, NULL},
show_someinfo, PF_INET, NF_IP_POST_ROUTING, 0);

第一个参数标明是不是链表头,初始化值为{NULL, NULL};第二个参数是回调函数,自定义原型如下

1
2
3
4
5
6
7
8
9
unsigned int uf_hook_test(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
printk(KERN_ALERT "Hello hook!\n");
return NF_ACCEPT;
}

第三个是协议族;
第四个是钩子的挂接点,有五个,分别为:

  • NF_IP_PRE_ROUTING 进入网络层而没有进行路由

  • NF_IP_FORWARD 接收到的数据向另一个网卡进行转发之前

  • NF_IP_POST_ROUTING 通过网络设备转发出去的包经过此点

  • NF_IP_LOCAL_IN 经过路由后,确定是本地报文时经过

  • NF_IP_LOCAL_OUT 本地报文做发送路由之前

第五个参数为优先级,值越小优先级越高,默认为继承,0值即NF_IP_PRI_FILTER

注册钩子函数

1
2
3
4
5
static int __init init()
{
return nf_register_hook(&show_ops);
}
module_init(init);

注销钩子函数

1
2
3
4
5
static void __exit exit()
{
nf_unregister_hook(&show_ops);
}
module_exit(exit);

通过数组可注册或注销多个钩子函数