网络编程-Socket
链路层以太网帧和ARP
以太网帧在首尾都要封装, 用来验证数据完整性. 其他IP报头, TCP段首, http协议头都只在头部封装.

ARP协议通过广播IP来来获得下一跳路由器的网卡地址. ARP数据报包括发送端的mac地址和IP地址, 以及接收端的mac地址和IP地址. 在发送时由于不知道接收端的mac地址, 所以填00:00:00:00:00:00, 当接收端(下一跳路由器)收到后, 发现IP地址和自己的一样, 就把自己的mac地址填充进数据报再广播传回去. 之后的每一跳路由器都对数据帧进行解封装再重新封装, 把最外层以太网帧的首尾解开, 再根据IP包中的目的IP地址选择路由表中的下一跳.
TTL: 最长生命周期, 传输的最大跳数. 当TTL减为0时, 当时的路由器就会把数据包丢弃.
MTU:maximum transmission unit,最大传输单元,由硬件规定,如以太网的MTU为1500字节。若一IP数据报大小超过相应链路的MTU的时候,主机会执行分片, 所以为了避免分片数据报大小一般不超过MTU.
MSS:maximum segment size,最大分节大小,为TCP数据包每次传输的最大数据分段大小,一般由发送端向对端TCP通知对端在每个分节中能发送的最大TCP数据。MSS值为MTU值减去IPv4 Header(20 Byte)和TCP header(20 Byte)得到, 等于1460。
TCP/UDP
TCP传输数据的路由通路是固定的, 只寻路一次, 所以稳定. UDP在每个路由器都要寻路, 所以不稳定.
TCP/UDP包含的是源端和目的端的16位端口信息, 其携带的数据可以超过以太网帧1500字节的限制, 只不过要分成多次传输; 或者链路层不使用以太网协议, 改用自定义的协议, 一次发送任意字节也行.
NAT和打洞
NAT映射: 以公网IP:端口号的形式把局域网IP映射到公网IP, 由路由器保存NAT映射表. 如浏览器的192.168.1.8:8080经过NAT映射到路由器后是公网IP:端口号, 之后再和目标网址通信.
打洞: 由于路由器会屏蔽第一次见到的陌生IP以防止攻击, 所以需要一个中转服务器(如腾讯, 其IP登录时已回传)把陌生路由器的IP登记, 这样两个陌生的路由器就可以直接通信了. 虽然路由器对第一次见到的包会丢弃, 但是会记录IP地址, 这样就算完成了打洞.
Socket网络通信
Socket绑定IP+端口号, 可以实现网络进程间通信. 其是一种借助缓冲区实现的伪文件, 可以全双工通信. Socket成对出现, 每边的一个fd对应发送和接收两个缓冲区, 从而实现双向通信.
Linux下占用存储的文件类型只有普通文件/目录/软链接, 硬链接是修改文件DEntry目录项属性得到的.
大端存储/小端存储
TCP/IP协议规定, 网络数据流应采用大端字节序, 即低地址高字节. 如0x12345678, 大端存储会在1003存78, 1002存56, 1001存34, 1000存12. 78是低地址, 却存放在了高字节处.
而x86平台默认小端存储, 1000存78, 1003存12, 即低地址低字节. 所以要做网络字节序和主机字节序的转换, l是32位长整型对应IP地址, s是16位短整型对应端口号.

"192.168.1.24"(字符串) -> 转成unsigned int -> htonl() -> 网络字节序
这种比较麻烦, 有函数可以直接把字符串ip变成网络字节序的二进制数, 如inet_pton()
"192.168.1.24" -> inet_pton(AF_INET, "192.168.1.24", &serv_addr.sin_addr.s_addr) -> serv_addr存放网络字节序IP
反过来就是inet_ntop(AF_INET, &serv_addr.sin_addr.s_addr, buf, sizeof(buf))
, buf存放IP字符串, 注意serv_addr.sin_addr.s_addr是int类型, 要取地址传入做参数.
sockaddr_in结构体
该结构体有三个成员, 协议类型, 端口号和ip地址, 后两者都要注意字节序问题.
原来的sockaddr结构体虽然已经被废弃了, 但有的函数接口还在用, 需要强转成(struct sockaddr*)类型. 另外accept()和connect()也要对sockaddr_in进行转型.
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons()/ntohs();
addr.sin_addr.s_addr = htonl()/ntohl()/inet_pton()/inet_ntop()
bind(sockfd, (struct sockaddr *) &addr);
socket()函数

sockfd = socket(AF_INET, SOCK_STREAM, 0)
创建sockfd.
bind()函数

bind(sockfd, &addr, sizeof(addr)
将socket()返回的sockfd绑定到服务器的一个ip+端口号, 使sockfd监听通信. 客户端不用调用bind()是因为客户端端口号无所谓, 系统会自动分配.
listen()函数
listen(sockfd, 128)
用于设置处于等待状态的最大客户端数量, 即最大同时建立连接数(不是保持连接数), 如三次握手队列, 默认128.
accept()函数

由服务端调用返回客户端sockfd, 所以传出参数sockaddr* addr返回的是客户端ip+端口号, 由服务器分配不用做初始化; socklen_t addrlen是传入传出参数, 在传入的时候是初始化客户端地址结构体的大小, 传出时是实际返回的客户地址结构体的大小.
socklen_t client_addr_len = sizeof(client_addr);
client_fd = accept(server_fd, &client_addr, &client_addr_len)
accept()返回的客户端sockfd用于和客户端通信, 和前面socket()创建的服务端sockfd不是同一个. 无客户端请求时, 服务端阻塞直到有客户端连接.
connect()函数
connect(sockfd, &addr, sizeof(addr))
由客户端调用, addr是服务端的ip和端口号, 需要已创建一个sockfd. 不同于bind()用于监听自身端口, connect()是向服务器发起连接.
由于client.c和server.c并不是一个线程, 所以分配的sockfd没有关联性, sockfd只是连接两个端口的中介, 而不是一个真正的文件.
netstat -apn | grep 6666
查看端口号占用情况, 需要先终止client后终止server, 否则端口号无法释放.
封装函数和系统函数同名的好处是可以进入帮助文档, 如socket()和Socket(), 首字母大小写不同不影响进入帮助文档, 但是可以封装错误处理之类的信息, 使代码更简洁. 其参数列和返回值必须和系统函数完全一样.
应该实现一个readn()和writen()函数, 每次从socket读写n个字节, 因为可能一个以太网帧1500个字节装不下要分多次传进来, 这时普通的read()会直接返回. 形如readn(cfd, buf, n), 每次读完后n-=1500(N-nread=nleft), 同时ptr+=1500, buf的一部分空间. 一般可以用来获取http头部信息.
另外, 读socket无法用fgets(), 需要再实现一个Readline()函数.
