星期三, 十月 15, 2008

"UNIX 网络编程"学习笔记

有点潦草,但总比没有强,放在博客上也好找,而且不用担心丢失:

(一) socket:

在 TCP 和 IP 之上的一个层

可靠:读出来一定是对的,但写入不一定能被对方收到。

while(1) {
ret = read(socket, fd, ...);
if(ret == 0 || ret == -1) {
break;
}
write(socket, fd, ...)
}
close(socket)

write BUG,部分写入,因为有 buffer
应该:
len = write(socket, fd ...)

close BUG, buffer 里面可能还有数据,如果此时就 close,就可能造成传输不完整。

BUFSIZE 不一致:
close 在 FIN 发出后就返回,不等 FIN+ACK 返回,对普通文件没问题,对套接字有风险
shutdown
LINGER 选项
用户层确认(应用层 ACK) 设计时精细点

STREAM 字节流
DGRAM 分组流,一次就是一个分组

UDP: SOCK_DGRAM
unreliable 对一个分组内部的数据还是可靠的,分组之间顺序不能确定

TCP Nagle 算法
小包消耗网络性能,所以小包先攒在 buffer 中,直到得到一个分段(MSS)或对方发出了一个确认

取消 Nagle: NODELAY 选项

writer
sendmsg


SOCK_SEQPACKET 是有序、双工的分组流

AF(Address Family) -> PF(Protocol Faminly) 为将来可能一个 Protocal 对应多个 Address
PF_LOCAL
PF_INET
PF_INET6

tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
0 - protocol: IPPROTO_TCP


ADDRESS FORMAT (man 7 ip)


字节序 对齐 问题
字节序:
大端(先高后低) 0x00000005
小端 x86 0x05000000

主机序/网络序

四字节对齐:
struct {
char c[5];
int k;
int n;
}
sizeof() 返回 16

struct {
char c[5];
int k;
int n;
}__attribute__(packet()); GCC 扩展 limitation

回避字节序和对齐问题:用流式套接字 ASCII 码通信——仿照 HTTP 协议
\r\n
fprintf
fputs
fgets
fread
fwrite
用标准 IO
fp = fdopen(sd, "r+")
把一个 sd 封装在 fd 中,
要记得通信完之后用 fflush()
效率会低一点,但网络效率不会比 CPU 高,CPU 不会成为网络的瓶颈

解决半连接:
SYNCOOKIE
取消 backlog 池

C --- SYN s=m-1 --> S
C <-- ACK a=m --- S
<-- SYN s=m-1 ---
C --- ACK A=m --> S

以前的 m 是随即的,现在用 SIP/DIP/SP/DP/协议号 等做一个线性变换做一个哈希来生成 m(SHA1),一分钟增一

但是 SYNCOOKIE 一些 TCP 的高级功能就不能用了,不符合 RFC 了。

Linux 策略:当半连接池耗尽时再使用 SYNCOOKIE 策略。

(二) TCP/IP
以太网(10M 共享) 快速交换式以太网 协议(通信格式和通信规程——协议设计)
格式:以太帧 MTU==1500(由硬件规定的),以太帧最长 1500+18=1518
规程:CSMA/CD——半双工 交换式——全双工

dst(6 bytes), src(6 bytes), protocol(2 bytes, 0800 IP)

单播
广播 0xFFFFFF
多播 0x01.... 0x010053..(IPv4 多播)

IP 层(RFC791 文档):
PP 点到点
MA: BMA, NBMA 广播型多点访问接口(以太)

格式:
/usr/include/netinet/ip.h
/usr/include/linux/ip.h
TOS: 最小 DELAY(telnet), 最大吞吐(ftp), 最低。。 最高可靠 虽然 TCP 端口号有此信息,但为了路由器设计的复杂度的降低,需要此冗余信息
tot_len: 16^2=64K,但以太网只能达到 1500 bytes,FDDI 也只能达到 4K
ttl: 最大跳数
protocol: (可以看 /etc/protocols)
check:

规程:编址、路由(如果两条路由规则都匹配,精确优先——路由表已经排过序)

ip ro sh/show
ip ro add x.x.x.x/m { via GATEWAY | dev NIC }

IP 单播/广播/多播
主机模式/网关模式 ip_forward

多播包默认 TTL 是 1,即默认多播不穿过路由器

TCP UDP
UDP: 多点通讯(广播 DHCP/多播 netmeeting) 单报(DNS)
流控、错序、丢报及其重传

TCP 32 位序号(字节序号,不是分组序号)/ 32 位 ACK 号(我下面希望收到你哪个字节!) 不是一个分组一个 ACK,可以几个连续一起 ACK
SACK(selected ACK, 选择性 ACK)

停等式流控(适合于小时延小传输局域网):
Bps——峰值流量 Pps(packet data量/时间)
Pps = t传/(t传 + Rtt)
Round Trap Time(RTT)
t传:传输时间
减小 RTT,增加 T传

滑动窗口(适合于大时延大传输网络):
一次多传点,则相当于 t传 增加了,这些报文在路由器 buffer 中: size(网络容量) = Rtt * Bandwidth(瓶颈带宽),则滑动窗口设置为一个网络容量最合适
TCP 探测网络容量——动态滑动窗口
接收方:大小 = 剩余网络容量/2
发送方:测出拥塞窗口 慢启技术(滑动窗口逐渐增大,一旦发现丢报则减半:加性增加,乘性减小)

点到多点:超时窗口而不是确认窗口

流媒体:开环无反馈控制!均匀速率

TCP 状态: flags


域名解析:
gethostbyname() —— 线程不安全
getaddrinfo() 取代 gethostbyname() 和 getservicebyname()

格式转换:
inet_ntop()
inet_pton()


man 7 setsockopt
getsockopt()
setsockopt()
SO_REUSEADDR 可解决 TIME_WAIT 状态
SO_RCVTIMEO SO_SNDTIMEO 否则用 alarm() 来处理超时(普世方法),或使用 select

使用 alarm() 处理超时:
sigsetjmp();
alarm(...);
...
recv()
...
alarm(0);

SO_RCVLOWAT/SO_SNDLOWAT
当 select 时可以设置最少有多少字节可读/可写时再从 select()/poll() 返回

SO_RCVBUF 不能设置得比 SO_RCVLOWAT 小!

SO_LINGER 都应该设置成 LINGER: 防止 close() 时还有数据,尽可能确保缓冲中的数据清空——控制 close() 的返回时机
在对端给出 FIN ACK 后才 close(),这时基本上可避免 TIME_WAIT 状态
完美解决:用户态确认,否则 LINGER + shutdown()

两个常规选项:SO_REUSEADDR/SO_LINGER

网络程序设置!!!
模拟大时延网络:利用 Linux 的 tc 程序(bps)
模拟丢包率:iptables(pps)
www.netfilter.org
patch-o-magic-ng*.tar.bz2 只要 random 功能
补丁:random,打在内核和 iptables 命令中
iptables -t filter -A FORWARD -m random --random-average 3 -j DROP

SO_REUSEPORT(BSD) 对数据报套接字有意义 多播/广播通信

man 7 tcp
TCP 层的 setsockopt 选项:
level = IPPROTO_TCP
TCP_NODELAY: 禁止 Nagle 算法,提高响应速度,但传输性能下降
TCP_QUICKACK(非标准): 快速确认,默认接受方滑动窗口多收几个再确认
TCP_CORK(非标准 Linux): 往吞吐率方向优化,响应速度下降 相当与一个塞子/闸门!
TCP_DEFER_ACCEPT(Linux): 推迟接受,当有 data 过来时 accept() 才返回!这样在 accept() 之后的 read() 不会阻塞,但可以用 select() 实现
TCP_WINDOW_CLAMP(Linux): 确认窗口大小 拥塞窗口

man 7 ip
IP 层 setsockopt 选项:
level = IPPROTO_IP
IP_TOS: 互联网不支持 TOS,永远是先来先服务,在内部网关有用
IP_TTL: 默认 64
/proc/sys/net/ipv4/ip_default_ttl
IP_HDRINCL: 原始套接字
IP_RECVERR:
IP_MTU_DISCOVER: 避免分片 P(Path)MTU_DISCOVER,会利用 ICMP
/proc/sys/net/ipv4/ip_no_pmtu_disc
IP_MTU
IP_MULTICAST_TTL
IP_MULTICAST_LOOP

man 7 packet
链路层套接字


数据报:不明确 C S
UDP 也可以 connect(),但是是过滤的意思

变长分组(DNS) 例如 struct
发方:struct, 利用 malloc() 分配堆
struct {
int len;
int math;
int liter;
char name[1]; # 比着实际长度再去分配堆
};
收方:
man 2 recvfrom
MSG_PEEK 收报先仅仅拷贝一次,获得报头,从而先知道 len,再根据 len 去 malloc()
UDP 报头有一个长度段,可以用 ioctl() 获取(man 7 udp),可以不用 int len; ioctl() 非阻塞,所以可借助于 select()


多播/广播,多点通信必须使用 UDP
man 7 ip
广播地址:全网 255.255.255.255
子网 ip ! ~netmask
只能作为目标地址出现
man 7 socket
SO_BROADCAST
令牌桶-->漏桶,首先利用内核的 TC 做流量控制,并把进程设置为实时(但只能尽力,因为是分时)

多播:D 类 224.0.0.0/4 28 bit 多播组号
RFC -- 如果分配多播地址
224.0.2.0 ~ 224.0.254.255

ping 224.0.0.1

互联网不支持多播,只能用隧道,并配置多播路由

多播 TTL 默认是 1
IP_MULTICAST_TTL
IP_MULTICAST_LOOP 是否自己要收到
IP_DROP_MEMBERSHIP
IP_ADD_MEMBERSHIP


IPv6 没有广播的概念,只有多播,广播只是多播的特例

在内网中发信息用多播比较方便,效率和网络利用率更高


(三) 信号:BSD 模型
每个进程有两个标志:mask/pending(32 位,每个位代表一个信号),mask 全为 1,pending 全为 0,调用 kill() 时内核将相应的 pending 位置 1,处理的时候将相应的 mask 置 0。信号在下次进程被内核调度的时候才响应(分时)

信号丢失:在响应处理之前两次发送了同一个信号
信号不排队

新的接口函数:sigqueue() 可排队

sigprocmask(); sigsuspend(); 原子操作
pause();

一个进程何时被调度:
1. 休眠
2. 从内核回到用户态的过程中
3. 时钟终端响应

实时信号是按照到来顺序相应的,并且内核会先相应标准信号
/usr/include/signal.h
/usr/include/bites/signum.h
#define SIGRTMIN
#define SIGRTMAX

signal 最好不要和线程同时使用!!!因为 signal 是针对进程的,所有线程都会收到信号,而哪个线程来处理是随机的,除非所有子线程都显示的关闭对信号的处理(mask()),只由一个线程来处理。

每个线程都有自己的 mask/pending,向该进程发信号会置所有线程的 pending,一个线程响应的时候会把所有的 mask 置位!
man pthread_sigmask
sigprocmask

硬件层中断

sig_automic_t
但并不能保证线程安全,线程仍然需要互斥(mutex),因为可能有多个 CPU!

平台相关:
不要用 signal(),用 sigaction()
setjmp()/longjmp(),用 sigjmp()

Python GIL?


(四) 高级 I/O
非阻塞 fcntl()
多路 select(), poll(), (Linux epoll() 原生态调用)
信号驱动 SIGIO fnctl()
异步 aio

select(),休眠最好使用 select(), sleep() 可能会影响 alarm() (非 UNIX 平台)——因为只有一个信号!在有些平台上 sleep() 很可能就是用 alarm() 实现的!看 man 3 sleep。要么调用 nanosleep()

pselect(), const sigset_t *sigmask 参数将:
sigprocmask() 和 select() 原子化了!

一次系统调用可看做一个原子操作!在用户态完全可以这样认为。系统调用的原子性不是靠锁来保证的。
最新:内核可抢占!
服务器编译内核:如果可能应该关闭内核可抢占功能, 参数 preemtive
i++ 的操作不会被中断打断,是比系统调用更微观尺度上原子操作

poll()
ppoll()

epoll() epfd 也可以用 epoll() 监视,因此可以构造一个链式的 epoll()

异步:之前都考虑是阻塞和非阻塞,即等待时不阻塞,但读写的时候还是可能会阻塞,这里是纯异步
适合于需要大量操作文件描述符的情况

man 3 aio_read
aio_read()/aio_write()
aio_error()
aio_return()
Linux IO 调度器(块设备 BIO)

aio_error() 会盲等!但很多时候不是问题。

/usr/include/aio.h
struct aiocb{
...
struct sigevent aio_sigevent;
}

这样就可以用 sigsuspend() 去等待信号了!

/usr/include/bits/siginfo.h
sigevent 可以有三种通知方式 notify
不通知
信号通知
线程通知: 一个函数指针
SIGEV_SIGNAL
SIGEV_THREAD

aio_cancel()
aio_fsync()
aio_suspend()

要连接动态库 lrt.so

man -k aio_

(五) OpenSSL
单钥加密
公钥加密: RSA

一般先交换公钥,然后商定一个单钥通道

证书解决中间人攻击

服务器证书
openssl genrsa -out server.key
openssl req -new -key server.key -out server.csr
openssl req -new -x509 -days 365 -key server.key -out server.crt

SSL/BIO 接口

BIO: man bio
bio_read()
bio_write()


Kerberos GSSAPI

RFC/ANSI/POSIX

Questions:
1. SSL?
2. 应用协议设计规范?
3. select 和标准 IO 问题?
4. 多进程、线程选择?
5. 有没有嗅探器可以直接看到应用协议(基于行的)? wireshark
6. 书上下两册?

wget ref 参数?

应用层协议设计方法:使用状态机!!!!!!

C 语言名字空间问题?


client_ssl.c
#include
#include

int main() {
SSL_CTX *myctx;
SSL *myssl;

myctx = SSL_CTX_new(SSLv3_client_method());
if () {

}

// SSL_use_RSAPrivateKey_file();
// SSL_use_certificate_file();

myssl = SSL_new(myctx);
if ()
{
}

SSL_set_fd(myssl, newsd);
// 绑定到 socket sd
ret = SSL_connect(myssl);

/* Verify certificate */

if (ret...) {
SSL_read();
SSL_write();
}

SSL_free(myssl);
SSL_CTX_free(myctx);
}


server_ssl.c
#include
#include

int main() {
SSL_CTX *myctx;
SSL *myssl;

myctx = SSL_CTX_new(SSLv3_server_method());
if () {

}

SSL_use_RSAPrivateKey_file();
SSL_use_certificate_file();

myssl SSL_new(myctx);
if ()
{
}

SSL_set_fd(myssl, newsd);
// 绑定到 socket sd
ret = SSL_accept(myssl);

if (ret...) {
SSL_read();
SSL_write();
}

SSL_free(myssl);
SSL_CTX_free(myctx);
}


test.c
#include
#define BUFSIZE 1024

int send_data(int sd, int fd)
{
int ret, len, pos;

len = 0;
pos = 0
while(1) {
if (len == 0) {
len = read(fd, buf, BUFSIZE);
pos = 0;
if (ret == -1) {
...
}
if (ret == 0) {
break;
}
}
ret = write(sd, buf+pos, ret);
len -= ret;
pos += ret;
}
close(sd);
}


multicast_test.c
#include

#include

int xx() {
...
int val = 1;
struct ip_mreqn mreq;

ret = setsockopt(sd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val));

inet_pton(AF_INET, "224.0.2.2", &mreq.imr_multiaddr);
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("eth0");
// 索引号: ip ad sh 时最前面显示的 id 值
ret = setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq));

ret = setsockopt(sd, IPPROTO_IP, IP_ADDMEMBERSHIP, &mreq, sizeof(mreq));
...
}

没有评论: