计算机网络

计算机网络

主要内容集中在网络层之上的协议知识

OSI模型

五层模型
应用层
传输层
网络层
数据链路层
物理层

数据链路层你

MAC地址

ARP协议

地址解析协议 将ip地址转换成mac地址

网络层

IP协议

ICMP协议

网际控制报文协议
确认ip包是否正确到达目标地址
通知在发送过程中ip包被丢弃的原因
ICMP基于IP协议工作 ICMP只能搭配IPv4 如果是IPv6则需要用ICMPv6
ICMP报文包含在IP数据报中 IP报头在ICMP报文的最前面 一个ICMP报文包括IP报头(至少20字节)、ICMP报头(至少八字节)和ICMP报文(属于ICMP报文的数据部分)

IGMP协议

互联网组管理协议
IP组播(多播)通信的特点是报文从一个源发出 被转发到一组特定的接收者 但在组播通信模型中 发送者不关注接收者的位置信息 只是将数据发送到约定的目的组播地址
要使组播报文最终能够到达接收者 需要某种机制使连接接收者网段的组播路由器能够了解到该网段存在哪些组播接收者 同时保证接收者可以加入相应的组播组中
IGMP就是用来在接收者主机和与其所在网段直接相邻的组播路由器之间建立、维护组播组成员关系的协议

路由选择协议

RIP协议

路由信息协议

OSPF协议

开放式最短路径优先

NAT

传输层

UDP

用户数据报协议
UDP在传送数据之前不需要先建立连接 接收数据的主机在收到UDP报文后不需要给出确认 因此UDP提供不可靠交付 但在某些情况下使用UDP很有效
并且UDP支持一对一 一对多 多对一 多对多的交互通信

使用UDP的协议

应用 名字 端口号
域名解析 DNS 53
简单文件传送 TFTP 69
路由选择协议 RIP
IP地址管理 DHCP
简单网络管理 SNMP 161
远程文件服务器 NFS
IP电话/多媒体通信 专用协议

UDP数据报格式

UDP数据报分为UDP首部和UDP数据部
UDP首部包括源端口 目的端口 长度 校验和(检测传输中是否出错 出错直接丢弃) 共8字节 每个部分2字节
运输层从网络层收到UDP数据报时 根据首部中的目的端口将UDP数据报发送到相应端口 最终上交到应用进程 若接收方发现UDP数据报中目的端口不正确(不存在对应端口号的进程) 就丢弃该报文 并通过ICMP发送端口不可达的差错报文给发送方
虽然UDP在通信的时候需要用到端口号 但UDP通信是无连接的 因此不需要使用套接字(TCP之间的通信必须要在两个套接字之间建立连接)

TCP

TCP传输控制协议
TCP提供面向连接服务 即在传送数据之前必须要先建立连接 数据传送结束后需要释放连接
TCP不提供广播多播服务 因为TCP提供可靠面向连接的服务 因此增加了许多无法避免的开销 如 确认 流量控制 计时器 连接管理等 使协议数据单元的首部增大了许多
TCP连接是一对一的 并且TCP提供全双工通信 即通信双方的应用进程在任何时候都能发送数据

使用TCP的协议

应用 名字 端口号
组播 IGMP
Email SMTP 25
远程终端 TELNET 23
超文本传输协议 HTTP 80
文件传送 FTP 21

TCP的连接

TCP连接的端点是套接字(Socket) 即端口号拼接IP地址就构成了套接字 例如(127.0.0.1:80)就是一个套接字
同一个IP地址可以有多个不同的TCP连接 同一个端口号也可以出现在多个不同的TCP连接中
套接字Socket与网络编程的socketAPI不是一个东西

TCP可靠传输的工作原理

停止等待协议

使用了停止等待协议:每发送完一个分组就停止发送 等待对方的确认到达后再发送下一个分组
超时重传:发送方发送完数据后超过一段时间仍没有收到确认 就认为刚才发送的分组丢失了 因而重新传送前面发送过的分组 关键在于发送完设置一个超时计时器 超过了预定时间没有收到回来的确认就再次发送 发送方发送分组后必须暂时保留已发送分组的副本 直到收到相应的确认才能清除副本 其次 分组和确认分组必须设置编号 最后 超时计时器设置的重传时间必须比数据在分组传输的平均往返时间更长 但如果这个重传时间设置非常长 通信的效率就会很低 重传时间设置太短就会产生不必要的重传
确认丢失:接收方收到了发送方的分组 但返回给发送方的确认在传输过程中丢失了 发送方超时重传了一份重复的分组 接收方又收到了重传的分组 需要丢弃这个重复的分组 并向发送方再次发送确认
确认迟到:接收方收到了发送方的分组 但返回给发送方的确认在传输过程中某种原因迟到了 因此发送方又发送了一份重复的分组 接收方又收到了重传的分组 需要丢弃这个重复的分组 并向发送方再次发送确认 发送方收到了重复的确认 也丢弃这个重复的确认
通过以上的确认和重传机制我们可以在不可靠的传输网络上实现可靠的通信

上述机制被整合为 自动重传请求ARQ意思是重传的请求自动进行 接收方不需要请求发送方重传某个出错的分组
但停止等待协议的信道利用率非常低 所以我们采用流水线传输:发送方连续发送多个分组 使信道上一直有数据不间断的传送 流水线传输需要使用 连续ARQ协议 滑动窗口协议

连续ARQ协议

连续ARQ协议规定 发送方每收到一个确认 就把发送窗口向前滑动一个分组的位置 接收方一般采用累积确认的方式 意思是接收方不需要对每个收到的分组逐个发送确认 而是收到几个分组后对按序到达的最后一个分组发送确认 表示所有分组都已经正确收到了
累计确认优点是容易实现 就算确认丢失也不需要重传 缺点是不能向发送方反映出接收方已经正确收到分组的信息 例如发送方发送前5个分组 中间第3个分组丢失了 接收方只能对前两个分组发送确认 发送方只能将后三个分组都重传一次 因此通信质量不好的时候连续ARQ会带来负面影响

TCP报文首部格式

一个TCP报文段分为首部和数据两部分 TCP报文段首部前20个字节是固定的 还有4n(n是整数)个字节根据需要而增加 因此TCP首部最小长度20字节
首部字段包含:
1.源端口和目的端口 各占2字节
2.序号 占4字节 指本报文段所发送的数据的第一个字节的序号 也称为报文段序号
3.确认号 占4字节=32位 可对4GB的数据进行编号 可保证序号重复使用 旧序号的数据早已送到终点了 是期望收到对方下一个报文段的第一个数据字节的序号
4.数据偏移 占4位 指出TCP报文段的数据起始处距离TCP报文段的起始处有多远 数据偏移的最大值是60字节
5.保留 6字节

6.控制位 6字节 用来说明本报文段的性质
6.1 紧急URG URG=1时说明紧急指针字段有效 通知系统此报文存在紧急数据 应优先传送 发送方TCP就把紧急数据插入到本报文段数据的最前面 例如ctrl+C
6.2 确认ACK ACK=1时确认号字段才有效 ACK=0时确认号无效 TCP规定 在连接建立后所有传送的报文段都必须把ACK置1
6.3 推送PSH 当两个进程进行交互通信时 有时在一端的进程希望在键入一个命令后立即就能收到对方的相应 TCP就可以使用推送 发送方TCP把PSH置1 并立即创建一个报文段发送出去 接收方TCP收到PSH=1的报文段 就立即交付接收进程 而不用等缓存都满了再向上交付
6.4复位RST RST=1时 表面TCP连接中出现严重错误 必须释放连接 然后再重新建立运输连接
RST=1还可以用来拒绝一个非法的报文段或拒绝打开一个连接
6.5同步SYN 在连接建立时用来同步序号 当SYN=1且ACK=0 表明这是一个连接请求报文 对方若同意建立连接 就在响应的报文段中让SYN=1且ACK=1 因此 SYN=1就表明这是一个请求连接或请求接受的报文
6.6终止FIN 用来释放连接 FIN=1时 表明此报文段的发送方的数据全部发送完毕 请求释放连接
7.窗口 2字节 窗口值是接收方告诉发送方设置发送窗口的大小 窗口值经常动态变化着
8.校验和 2字节
9.紧急指针 2字节 紧急指针仅在URG=1的时候才有意义 指出本报文段紧急数据的字节数 窗口为0时也可以发送紧急数据
10.选项 长度可变 最长40字节 长度为0时TCP首部长度为20字节

随着互联网的发展还增加了许多功能

TCP的运输连接管理

运输连接一共三个阶段:连接建立 数据传送 连接释放

TCP连接建立

TCP建立连接的过程叫握手
A为TCP客户端 B为TCP服务端

A向B发出连接请求报文段 这时TCP首部中的SYN=1 同时选择一个初始序号seq=x A进入同步已发送状态
(TCP规定SYN报文段不能携带数据 但要消耗掉一个序号)

B收到连接请求的报文后 若同意建立连接 就向A发送确认报文 在确认报文段中 SYN=1 ACK=1 确认号ack=x+1 同时也为自己选择一个初始序号seq=y 这个报文段也不能携带数据 同样也要消耗掉一个序号 B进入同步收到状态

A收到B给出的确认报文后 还要再向B发送确认 确认报文ACK=1 确认号ack=y+1 而自己的序号seq=x+1 (ACK报文段可携带数据 若不携带数据就不消耗序号 这种情况下 下一个数据报的序号仍是seq=x+1) 至此 TCP连接已建立 A进入已建立连接状态
B收到A确认后也进入已建立状态

A最后还要发送一次确认的原因: 防止已失效的连接请求报文段因为网络堵塞的原因突然又传送到B从而发生错误 若B收到已失效的连接请求报文后 会向A发送确认报文 A收到后不会向B发送确认 B由于收不到确认就知道A没有要求建立连接

TCP连接释放

数据传输结束后 AB都处于已建立连接状态 A把连接释放报文段首部的FIN=1 序号seq=u(u=前面已传送过的数据的最后一个字节序号+1) A进入FIN-Wait-1状态等待B的确认(FIN报文段即使不携带数据 也消耗掉一个序号)

B收到连接释放报文段后发出确认 确认号ack=u+1 而这个报文段自己的确认号是v(等于B前面传送过的数据的最后一个字节的序号+1) 然后B就进入关闭等待阶段 此时TCP处于半关闭阶段:A没有数据发送给B B仍能给A发送数据A仍能接收 A收到来自B的确认后 进入FIN-Wait-2状态 等待B发出的连接释放报文段

若B已经没有向A发送的数据 B向A发出连接释放报文段中FIN=1 假定B的序号是w (半关闭的状态下B可能又向A发送过一些数据) B还必须重复上次已发送过的确认号ack=u+1 此时B进入最后确认状态 等待A的确认

A收到B的连接释放报文段后 对B发出确认 确认报文段中ACK=1 确认序列号ack=w+1 自己的序号是seq=u+1 然后进入Time-wait状态 经过时间等待计时器设置的时间后(2个最长报文段寿命) A才进入关闭状态 B收到A发来的确认后直接进入关闭状态 因此B结束TCP连接的时间比A要早

A需要等待2个最长报文段寿命的原因: 1.为了保证A发送的最后一个ACK确认报文能够到达B 如果这个报文段丢失 B无法收到A的确认报文 B就会重传给A FIN=1和ack的连接释放报文 A在这段时间内就能收到这个重传的FIN+ACK报文 接着A就会重传一个确认 报文准确送达后 A等待一段时间后和B都能进入关闭状态 2.防止已失效的连接请求报文段出现在此连接中

A与B建立连接后 A出现故障: TCP设置了一个keepalive timer(保活计时器) A故障后B无法收到A发来的数据 因此应有措施使服务器不白白等待下去 服务器B每一次收到A的数据时都会重新设置保活计时器 超过时间间隔都没收到A发来的数据时 B发送一个探测报文段 每隔75s发送一次 发送10个探测报文后仍无客户响应 B就认为A出了故障 就关闭连接

应用层

DNS域名系统

通过域名服务器程序将域名解析成ip地址 过程如下:某一个应用进程需要把主机名解析成ip地址时 该应用进程调用解析程序 成为DNS的一个客户 将待解析的域名放在DNS请求报文中 以UDP形式发送给本地域名服务器 使用UDP是为了减少开销 本地域名服务器在查找域名后把对应的ip地址在回答报文中返回 应用进程获得目的主机ip地址后开始通信

万维网www

统一资源定位符URL

<协议>://<主机>:<端口>/<路径>

超文本传输协议HTTP

HTTP默认端口号80 HTTP是一个面向事务的应用层协议 是能在万维网上可靠的交换文件的重要基础
HTTP虽然使用了面向连接的TCP作为运输层协议 但HTTP本身是无连接的 即通信的双方交换HTTP报文之前不需要先建立HTTP连接
HTTP协议是无状态的 即同一个客户第二次访问同一个服务器上的页面时 服务器的响应与第一次被访问时的相同 因为服务器并不记得曾经访问过的这个客户 也不记得为这个客户曾经服务过多少次 HTTP的无状态特性简化了服务器的设计 使服务器更容易支持大量并发的HTTP请求

为了保留HTTP无状态的特点 又要让服务器记住之前的客户 引入了cookie技术/http1.1的持久连接 即中间不停的发送确认应答
在没有cookie信息的状态下的请求时 服务器在响应中添加cookie后返回客户 客户端保存cookie
在下一次有cookie信息的请求中 自动在请求中添加cookie发送 服务器端检查cookie的sid后相应

如果客户端禁用了cookie sid字段客户端用的方法是URL重写 即在URL里加上sid字段 这样sid就不用回传给客户端 只在每次页面跳转的时候把sid写进URL

cookie与session区别

  1. cookie把用户数据存到本地 session把用户数据存在服务器
  2. 单个cookie大小不超过4kb 单个session大小与服务器内存有关
  3. cookie不安全 可以分析cookie进行cookie欺骗 session能更安全
  4. cookie在没禁用的情况下会成为sid值的容器 可以把重要信息放在session中 其他信息放在cookie中

HTTP报文结构

HTTP有两种报文
请求报文:客户向服务器发送请求报文
响应报文:服务器应答客户的报文
请求报文和响应报文都是由三个部分组成 区别是开始行不同

开始行

用于区分请求报文和响应报文 请求报文中的开始行叫请求行 响应报文中的开始行叫状态行
HTTP请求报文由请求行 请求头部 空行 请求数据 四个部分组成
HTTP响应报文由状态行 消息报头 空行 相应正文 四个部分组成
请求行包含了请求方法+空格+URL+空格+HTTP版本+CRLF(回车换行)
状态行包含了HTTP版本+空格+状态码+短语+CRLF

GET请求报文
1
2
3
4
5
6
7
8
9
10
GET /562f25980001b1b106000338.jpg HTTP/1.1   #请求行
Host:img.mukewang.com #空行前都是请求头部
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) #HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8 #说明用户代理可处理的媒体类型。
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch #说明用户代理可处理的媒体类型。
Accept-Language:zh-CN,zh;q=0.8 #说明用户代理能够处理的自然语言集。
空行
请求数据为空 #请求数据也叫主体
POST请求报文
1
2
3
4
5
6
7
8
POST / HTTP1.1 #请求行
Host:www.wrox.com #空行前是请求头部
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley #请求数据
HTTP响应报文
1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK    #状态行 由HTTP版本号 状态码 状态消息 组成
Date: Fri, 22 May 2009 06:07:21 GMT #二三行为消息报头 说明客户端要使用的信息
Content-Type: text/html; charset=UTF-8
空行
<html> #响应正文
<head></head>
<body>
<!--body goes here-->
</body>
</html>
请求方法
方法 含义
GET 请求读取由URL标志的信息
POST 给服务器添加信息
OPTION 询问支持的方法有哪些
HEAD 请求读取由URL所标志的信息的首部
PUT 在指明的URL下存储一个文档(传输文件)
DELETE 删除指明的URL所标志的资源(删文件)
TRACE 用来进行环回测试的请求报文
CONNECT 用于代理服务器
GET/POST区别

get是从服务器上获取数据 post是向服务器传送数据
get后退按钮/刷新无害 post数据会被重新提交(浏览器会弹窗告知用户数据会被重新提交)
get书签可收藏 post为书签不可收藏
get能被缓存 post不能 除非人工修改设置
get编码类型为URL post支持多重编码方式
get对数据长度有限制 URL最大长度2048个字符 post无限制
get安全性较差 发送信息都显示在URL上 post的数据不会显示在URL中
get传递的参数都是ASCII字符 post没有限制
get产生一个tcp数据报 post产生两个tcp数据报
get请求时 浏览器会把http的header和data一起发送出去 服务器响应200(返回数据)
post请求时 浏览器先发送header 服务器响应100(continue) 浏览器在发送data 服务器响应200 但Firefox的post就只发送一次

状态码种类

1xx:表示通知信息 比如请求收到或正在处理
2xx:表示成功 202接受
3xx:表示重定向
4xx:表示客户的差错 400错误请求 403拒绝执行 404不可用
5xx:表示服务器的差错 502网关无响应 503服务不可用 504网关超时 505HTTP版本不支持

首部行

用来说明浏览器 服务器 报文主体的一些信息 首部行可以有好几行 也可以不使用 每一个首部行都包含首都字段名和它的值 每一行结束都有CRLF 首部行结束后还要隔一个空行与后面的实体主体分开

实体主体(entity body)

请求报文中一般都不用这个字段 有些响应报文中也可能不用

HTTPS

HTTPS是对HTTP的一个延展 在TLS(Transport Layer Security)或SSL(Secure Socket Layer)之上 常用端口443
HTTPS加密所有数据 包括报文头部请求/响应报文

HTTPS通信原理
  1. 客户使用https的URL访问web服务器 要求与服务器建立ssl连接
  2. 服务器收到请求后 将网站的证书信息(包含公钥)返回给客户端
  3. 客户端的浏览器与web服务器协商ssl连接安全等级
  4. 客户端浏览器协商完毕 建立会话密钥 然后利用网站给的公钥将会话密钥加密冰川送给服务器
  5. 服务器利用自己的私钥解谜出会话密钥
  6. 服务器利用会话密钥加密与客户端的通信

网络编程

socket

创建socket

1
2
3
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

domain代表底层协议簇 type参数指定服务类型 服务类型有流服务(Sock_Stream) 数据报服务(Sock_Ugram) 对于TCP/IP协议簇来说 流服务指TCP协议 数据包服务指UDP协议 Linux2.6.17后 服务类型新增了 Sock_Nonblock(创建的socket为非阻塞的) Sock_Cloexec(用fork调用创建子进程时在子进程中关闭该socket)
protocol参数指在前两个参数构成的协议集合下选择一个具体的协议 一般所有情况下都把它设置为0表示使用默认协议
socket()调用成功返回一个socket文件描述符 失败返回-1 并设置errno

命名socket

将一个socket与socket地址绑定成为命名socket 服务器程序需要命名socket 只有命名后客户端才知道如何连接它 客户端则通常不需要命名socket采用匿名方式(使用系统自动分配的socket地址)

1
2
3
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

bind()将my_addr所指的socket地址分配给未命名的sockfd文件描述符 addrlen指出socket地址的长度
bind()成功返回0 失败返回-1 并设置errno 两种常见的errno为 EACCES(被绑定的地址是受保护的 只有root能访问) EADDRINUSE(被绑定的地址正在使用 比如将socket绑定到一个处于TIME_WAIT状态的socket地址)

监听socket

socket命名后需要系统调用创建一个监听队列来存放待处理的客户连接

1
2
#include<sys/socket.h>
int listen(int sockfd, int backlog);

sockfd指定被监听的socket backlog提示内核监听队列的最大长度 若超过最大长度 服务器不受理新的客户连接 客户端也将受到ECONNREFUSED错误信息 Linux2.2之后 backlog指标是处于完全连接状态(ESTABLISHED)的socket的上限 处于半连接状态(SYN_RCVD)的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog定义 backlog一般是5
listen()成功返回0 失败返回-1 并设置errno

接受连接

1
2
3
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd是listen后的监听socket addr获取被接受连接的远端socket地址 长度为addrlen
accpet成功返回一个新的连接socket 该socket唯一的标识了被接受的这个连接 服务器可通过读写该socket来与被接受连接对应的客户端通信
accept失败返回-1 并设置errno
accept()对于客户端网络断开毫不知情 accept只是从监听队列中取连接 不管连接处于何种状态 也不关心网络变化

发起连接

客户端通过connect来主动与服务器建立连接

1
2
3
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t *addrlen);

sockfd参数由socket系统调用返回一个socket serv_addr是服务器监听的socket地址 长度为addrlen
connect成功返回0 一旦成功连接 sockfd唯一标识这个连接 客户端可以通过读写sockfd来与服务器通信
connect失败返回-1并设置errno 常见的errno ECONNREFUSED(目标端口不存在 连接被拒绝) ETIMEDOUT(连接超时)

关闭连接

1
2
#include<unistd.h>
int close(int fd)

fd是待关闭的socket close并非总是关闭一个连接 只是将fd的引用计数-1 只有当fd=0的时候才真正关闭连接 多进程程序中 一次fork()默认将父进程中打开的socket引用计数+1 因此我们必须在父进程和子进程中都对该socket执行close才能关闭连接

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

sockfd指待关闭的socket howto决定如何关闭 取值有SHUT_RD(关闭sockfd上读的这部分 若应用程序不再针对socket文件描述符进行读操作 就丢弃) SHUT_WR(关闭sockfd上写的这部分 sockfd的发送缓冲区中的数据会在真正关闭连接前全部发送出去 应用程序不可再对该socket文件描述符执行写操作 这时的情况下连接处于半关闭状态) SHUT_RDWR(同时关闭sockfd上的读和写)
shutdown()能分别关闭或者都关闭读和写 close只能将读写同时关闭
shutdown成功返回0 失败返回-1 并设置errno

数据读写

TCP数据读写
1
2
3
4
5
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags)

ssize_t send(int sockfd, const void *buf, size_t len, int flags)

recv读取sockfd上的数据 buf和len指定读缓冲区的位置和大小 flag通常设置为0
recv成功返回读取到的数据长度 可能小于我们期望的长度 因此我们可能需要多次调用recv才能读取到完整的数据
recv返回0的时候一位置对方关闭连接 recv出错返回-1 并设置errno

send往sockfd上写入数据 buf和len指定写缓冲区的位置和大小 send成功时返回实际写入的数据的长度
send失败返回=1 并设置errno

UDP数据读写
1
2
3
4
5
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

ssize_t send(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)

recvfrom读取sockfd上的数据 buf和len指定读缓冲区的位置和大小 因为UDP通信没有连接的概念 所以每次读取数据都要获取发送端的socket地址 即src_addr 地址的长度为addrlen
sendto往sockfd上写入数据 buf和len指定写缓冲区的位置和大小 dest_addr指定接收端的socket地址 地址的长度为addrlen

recvfrom和sendto也可以用于TCP连接 只需要把最后两个参数设置为NULL 因为TCP是面向连接的 已经连接了就知道对方的socket地址了

通用读写函数
1
2
3
4
#include<sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

sockfd指定被操作的目标socket msg是msghdr结构体的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct msghdr{
void *msg_name; //socket地址 TCP中被设置为NULL
socklen_t msg_namelen; //socket地址长度
struct iovec* msg_iov; //分散内存块
int msg_iovlen; //分散内存块数量
void *msg_control; //指向辅助数据的起始位置
socklen_t msg_controllen; //辅助数据大小
int msg_flags; //复制函数中的flags参数 在调用中更新
}

struct iovec{//分散内存块
void *iov_base;//内存起始位置
size_t iov_len;//内存的长度
}

带外标记

就是紧急数据需要接受 内核通知应用程序由带外数据到达的两种常见方式 I/O复用产生的异常事件和SIGURG信号
应用程序收到了需要接受带外数据的通知 还要知道带外数据在数据流的具体位置才能准确接收

1
2
#include<sys/socket.h>
int sockatmark(int sockfd);

sockatmark判断sockfd下一个读取到的数据是否是带外数据 是就返回1 利用带MSG_OOB标志的recv调用来接收带外数据 不是就返回0

地址信息函数

1
2
3
#include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr *address, socklen_t *address_len);
int getpeername(int sockfd, struct sockaddr *address, socklen_t *address_len);

getsockname获取sockfd对应的本端socket地址 并存入address指向的内存中 长度为address_len
若实际的socket地址长度大于address指向的内存大小 socket地址会被截断
getsockname成功返回0 失败返回-1 并设置errno
getpeername获取sockfd对应的远端socket地址 参数以及返回值与getsockname相同

socket选项

专门用来读取和设置socket文件描述符属性的方法

1
2
3
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t *restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t option_len);

level指定操作协议的属性(IPV4 IPV6 TCP….) option_name指定选项的名字option_value option_len指定被操作的值和长度

socket选项

getsockopt/setsockopt成功返回0 失败返回-1 并设置errno
对于服务器而言 有部分socket选项只能在调用listen前针对socket设置才有效 因为socket只能由accept调用返回 而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(监听队列中的连接至少已经进入了SYN_RCVD状态) 说明服务器已经往被接受连接上发送了TCP同步报文段
可是有的socket选项应该在TCP同步报文段中设置 例如TCP最大报文段选项 对这种情况 Linux给出的解决方案是:对监听socket设置这些socket选项 那么accept发挥的连接socket将自动继承这些选项 对客户端而言 这些socket选项应该在调用connect之前设置 因为connect调用返回后说明TCP三次握手已经完成

下面我们详细讨论部分重要的socket选项

SO_REUSEADDR选项

设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT连接占用的socket地址
经过setsockopt设置选项为SO_REUSEADDR后 即使sock处于TIME_WAIT状态 与之绑定的socket地址也可以立即被重用 此外我们可通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket 从而使TCP连接根本不进入TIME_WAIT状态

SO_RCVBUF/SO_SNDBUF

SO_RCVBUF/SO_SNDBUF 分别表示TCP接收缓冲区和TCP发送缓冲区的大小 我们setsockopt设置大小值后 系统会将其加倍 使其不小于某个最小值 例如TCP接收缓冲区最小值256字节 发送缓冲区最小值2048字节 系统如此做是为了确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(快速重传就期望TCP接收缓冲区至少能容纳4个大小为SMSS的TCP报文段)
修改TCP发送接收缓冲区大小的值文件在proc/sys/net/ipv4/tcp_rmem tcp_wmem

SO_RCVLOWAT/SO_SNDLOWAT

SO_RCVLOWAT/SO_SNDLOWAT分别表示TCP接收缓冲区和发送缓冲区的低水位标记 一般被I/O复用系统调用来判断socket是否可读或可写
当TCP接收缓冲区中可读数据的总数大于其低水位标记时候 I/O复用系统调用将通知应用程序可以从对应的socket上读取数据
当TCP接收缓冲区中可写数据的总数大于其低水位标记时候 I/O复用系统调用将通知应用程序可以从对应的socket上写入数据
默认情况下的低水位标记为1字节

SO_LINGER

用于控制close关闭TCP连接时候的行为

网络信息API

socket地址两个要素 IP地址 端口号 都用数值表示 因此我们用主机名表示IP 服务名表示端口号

gethostbyname/gethostbyaddr

gethostbyname函数根据主机名称获取主机完整信息gethostbyname先在etc/hosts文件中查找主机 找不到去访问DNS gethostbyaddr根据IP地址获取主机完整信息

1
2
3
#include<netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, size_t len, int type)

name主机名 addr主机IP地址 len指定addr长度 type指定ip地址类型
两个函数返回值都是hostent结构体类型的指针

1
2
3
4
5
6
7
8
#include<netdb.h>
struct hostent{
char *h_name; /*主机名*/
char **h_aliases;/*主机别名 可能有多个*/
int h_addrtype; /*地址类型*/
int h_length; /*地址长度*/
char **h_addr_list; /*按网络字节序列出的主机IP地址列表*/
};
getservbyname/getservbyport

getservbyname获取某个服务完整信息 getservbyport根据端口号获取服务完整信息 都是读取/etc/services文件的

1
2
3
4
#include<netdb.h>
struct servent *getservbyname(const char *name,const char *proto);

struct servent *getservbyport(int port, const char *proto);

name服务名称 port对应端口号 proto指定服务类型(tcp udp null全部)
返回值为servent结构体类型的指针

1
2
3
4
5
6
7
#include<netdb.h>
struct servent{
char *s_name;/*服务名称*/
char **s_aliases;/*服务别名*/
int s_port;/*端口号*/
char *s_proto;/*服务类型*/
}
getaddrinfo
1
2
#include<netdb.h>
int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result);

hostname主机名 service服务名 hints可设置NULL result指向一个链表存储getaddrinfo反馈结果

End of reading! -- Thanks for your supporting