Linux网络编程
一、网络通信的基本概念
1. OIS模型和TCP/IP模型
OIS模型的分层以及对应的TCP/IP模型分层
TCP网络模型
2.协议
0.以太网帧的数据格式
-
目标地址和源地址是网卡的硬件地址(mac地址),占用48位,6个字节。
可以在shell中使用ifconfig查看 -
类型字段占两个字节,指定协议类型。
协议字段可以有三种值,0800 对应IP数据报,0806对应ARP请求/应答,0835对应RARP 请求应答。 -
以太网帧中的数据长度规定最小是46个字节,最大1500字节。ARP和RARP数据报的长度不够,需要在后面补位。
-
最大值1500 成为以太网最大传输单元MTU,不同的网络类型有不同的MTU。注意MTU这个概念指数据帧中有效负荷的最大长度,不包括帧头长度。
1 ARP报文格式
-
由于源主机只知道目标主机的IP地址和端口号,不知道其物理地址。而每台机器的IP地址都是通过其网卡指定的,所以如果只知道IP地址而不知道网卡的mac地址,是无法访问到该机器的。(网卡属于物理层而IP属于网络层,所以首先要网卡接收到数据包,之后才能解析IP地址,物理地址优先于IP地址)。所以通讯前,源主机会广播发出ARP请求,对应IP地址的主机将其MAC值进行返回。
ARP协议用于在通信前获取通信对象的硬件地址。 -
每个主机都为维护一个ARP缓存表,可以用arp -a 查看
-
ARP报文格式
2 IP数据包格式
-
版本号 Version:长度4bit,一般0100 指IPV4 。0110指IPV6
-
首部长度 Header Length:占4位一个字节,用来描述IP包头部的长度。 每一行大小位32个位,即4个字节。头部一共占用5行,即54 = 20个字节。长度最长位1111 即154 = 60个字节,最小位20个字节。
-
服务类型 Type of Service:长度8bit,1个字节。按位被定义如下,PPP DTRC0,定义包的优先度,取值越大,优先级越高,数据越重要
-
IP包总长Total Length: 长度16bit以字节为单位来计算IP包的长度,最大为65535字节
-
标识符 Identifier :长度16bit。和Flags Offset字段联合使用,对较大的上层数据包进行分段操作,
-
标记 Flags :长度3bit,第一位通常不同,第二位是DF(don’t fragment)位,DF位设为1表示路由器不能对该上层数据包分段。第三个位是MF(more fragment),当路由器对一个上层数据包分段,则路由器会在除了最后一个分段的IP包的包头中将MF设为1
-
片偏移 Fragment Offset:长度13bit,表示该ip包在该组分片包中的位置,接收端靠此来组装还原IP包
-
生存时间 TTL Time to Live: 长度8bit,用来表示IP包最多被多少个沿途路由转发
-
协议 Protocol :长度8bit,用来标识上层用的协议。常用的协议号如下:
1 ICMP 2 IGMP 6 TCP 17 UDP 88 IGRP 89 OSPF -
头部校验 Header Checksum :长度16bit,用来检测IP头部的正确性,但不包含数据部分
-
起源和目标IP地址 Source and Destination Address :两个地址都是32bit,标准了这个IP包的起源和目标地址。除非使用了NAT,否则整个传输过程中,这两个地址不会改变。
3 ICMP协议
- ICMP协议 Internet Control Message Protocol ,是以太网控制信息协议,是TCP/IP族的一个子协议,主要用于在主机、路由器之间传递控制消息,包括报告错误,交换受限控制和状态信息等等。
- 控制信息是指,网络不同,主机是否可达,路由是否可用等网络本身的信息。当遇到IP数据无法访问目标,IP路由器无法按当前的传输速率发送数据包等情况时,就会自动发送ICMP消息。
- ICMP是一个面无无连接的协议,用于传输出错报告控制信息.
- ping命令也是基于ICMP协议的。
4 TCP协议
-
TCP通信的特点
- 面向连接的传输层协议,提供双全工和可靠的交付服务。
- TCP所发送的每一个报文段加上序列号,保证每一个报文能被接收方接收,并且只被正确的接收一次。
- 采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。
- 采用可变滑动窗口协议进行流量的控制,以防止由于发送端和接收端之间的不匹配而引起的数据丢失。
- 因TCP具有重传机制,除纯粹的ACK报文不需要回复外,TCP每发送一条消息都需要等待对方的回复ACK,如果超时未回复的话,发送端会将该数据进行重发,重发超过规定次数,则认为连接出错。
- TCP通信的不稳定因素在于:1.数据在网络上的传输速度不一定,其到达的先后次序也不一定2.因此结合重传机制,在网络中可能存在多条同样的数据报
-
TCP报文头部格式
- 源端口,目的端口: 16bit,标识出发送端和接收端的端口号
- 顺序号 sequence number:32bit 标志出发送数据报的编号
- 确认号 acknowledge numbwe:32bit标志出下一次希望对方发送的序列号的编号
- TCP头长:4bit。表明TCP头中包含了多少个32位字(有多少行);
- 4位未用
- URG:进制指针有效(urgent pointer),紧急指针支出本报文段中的紧急数据的最后一个字节的序号。
ACK: ACK位置为1,表明确认号有效。如果ACK为0,则表示数据报中不包含确认号,或者确认号被忽略。
PUSH:表示是带有PUSH标志的数据,当PUSH为1时,不对数据包采用缓冲策略,数据包一到就送到应用程序那里(请求方)。
RST:用于复位由于主机崩溃或其他原因而出现的错误的连接,或用于拒绝非法的数据报和连接请求。当RST为1时民表示TCP连接中出现了严重的差错,必须释放连接,然后重新建立连接
SYN:用于建立连接。当SYN = 1时,表示发起一个连接请求
FIN:用于释放连接。当FIN = 1时,表明此端的发送数据已经发送完成,并要求释放连接 - 窗口大小:16 bit。窗口大小字段表示了在确认了字节之后还可以发送多少个字节。此字段可以进行流量控制,单位为字节数,代表本机期待一次接收的字节数。
- 校验和:16 bit。为了确保高可靠性而设置的,它校验头部,数据和伪TCP头部之和
- 可选项
- IP地址信息在IP包的头部,TCP包提供的是端口号
TCP的三次握手
-
目的:1.为了确保TCP连接的成功建立,提供可靠的连接通信。,2.对通信双方可使用的序列号进行确认
-
三次握手步骤:
1.客户端向服务器发送连接请求报文、告诉服务器自己的seq: SYN=1,seq = x;
2.服务器接收到请求后确认到客户端的seq、需要回复自己的seq,向客户端回复: SYN= 1,ACK=1,seq=y,ack = x+1;
3.客户端确认到服务器的seq,向服务器发送 :ACK=1,seq = x+1,ack=y+1;
当3次握手进行后,主机就会认为连接已经建立,可以进行数据的收发。 -
为什么需要三次握手而不是两次:
1 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误
2 按照两次握手协议,服务器端在发送完确认报文后,认为连接已经建立,随后就向客户端发送数据,而此时如果确认报文丢失,则客户端认为连接仍未建立,会忽视后面发送的报文。即第二次握手的报文丢失,服务器也不会重发,造成客户端无法获知服务器的序列号,第二次握手无法建立。 -
第三次握手时,数据丢失会怎么样?
1 服务器端未接收到第三次确认报文,会先进行超时重发,超过一段时间后认为连接出错,断开此连接,客户端此时需要重新进行连接
2 如果此时客户端发送了数据,服务器端可以正常接收(序列号一致),并将其视作第三次握手,成功建立连接。
TCP的四次挥手
-
背景:1.TCP连接是双全工的,因此每个方向都必须单独进行关闭。2.发送FIN时意味着主机已经确认,本端所有数据都已发送,没有需要发送的数据了。接收到FIN,意味着对向不会在发送通信数据过来了。
-
关闭流程,A->B A主动关闭
- A准备就绪,关闭通信数据发送端,向B发送 FIN=1 seq = u;
- B收到FIN信号,进入准备关闭状态,检查是否有仍未发送的数据。向A发送 ACK = 1 ack = u+1 seq = v;
- B确认没有需要再发送的数据,关闭数据发送端,等待最后一次确认。向A发送FIN = 1 ACK = 1 seq=w ack=u+1;
- A收到B的关闭信息,进入TIME_WAIT状态,给B发送ACK=1,seq=u+1,ack=w+1;B此时可以正常关闭。A等待2MSL之后关闭。
-
图示如下
+ TCP状态说明:
CLOSED:初始关闭状态
LISTEN: 服务器的某个套接字处于监听状态,可以接受连接了
SYN_RCVD:表示接收到了SYN报文,等待第三次握手回复ACK报文
SYN_SEND:表明客户端已经发送了SYN报文
ESTABLISHED :已建立连接状态
FIN_WAIT_1:等待对方回复FIN信号的第一个阶段,对方回复ACK报文后立刻进入第二阶段
FIN_WAIT_2:等待对方回复FIN信号的第二个阶段,已经收到了ACK报文,等待对方发送FIN报文,处于半连接状态。
TIME_WAIT:已经收到了对方的FIN报文,并发送了ACK报文,等待2MSL后即可回到CLOSED状态。
CLOSING:比较少见,当发送FIN后,没有收到对方的ACK报文,反而收到了FIN报文时会进入此状态。在双方都进行关闭操作时会出现
CLOSE_WAIT:收到对方的FIN请求,检测己方是否还有需要发送的数据,
LAST_ACK:被动关闭的一方发送FIN报文后,最后等待对方的ACK报文的状态,接收到ACK后就可以关闭了。 -
为什么需要TIME_WAIT状态等待2MSL?
1.防止第四次挥手报文丢失对方没有收到。
2.保证通信没有残留的通信数据,影响到下一次的通信。 -
打开关闭全流程图示
5 UDP协议
- 概述
UDP 即用户数据报协议,是一种面向无连接的协议,类似短信,收发数据前无需和对方建立通信连接直接发送即可。因此一个UDP的套接字既可以作为服务器也可以作为客户端。 - UDP每次发送数据不需要等待对方的回信,而且不需要连接,所以UDP的传输速度会比TCP高,常用语实时性要求比较高的场合。使用UDP的协议包括:TFTP,SNMP,NFS,DNS,BOOTP等
- UDP数据包的包头格式
1.源端口+2.目标端口+3.长度+4.校验和
二、SOCKET编程
1.socket基本概念
-
1.1 socket概念
- socket是一种ipc方法,它允许位于同一主机或使用网络连接起来的不同主机上的应用程序之间交换数据。
- socket是一种特殊的io接口,它也是一种文件描述符。是一种常用的进程间通信的机制。
- 每一个socket都用一个半相关描述来表示。{协议、本地地址、本地端口号)
- 一个完整的套接字则用一个相关描述来表示{协议,本地地址、本地端口号,目标地址、目标端口号}。
-
1.2 socket的通信领域 domain
- socket存在于一个domain中,它用以确定1.识别出一个socket的方法 2.通信范围
- 通信domain分类:
1. UNIX domain (AF_UNIX) 用于同一主机之间的进程通信
2. IPv4 domain (AF_INET) 使用IPv4协议连接起来的主机之间的进程通信
3. IPv6 domain (AF_INET6) 使用IPv6协议连接起来的主机之间的进程通信 - AF 表示 address family ,PF表示 protocol family
-
1.3 socket类型
- 流式socket (SOCK_STREAM)
提供可靠的,面向连接的通信流。TCP是此类型的socket,从而保证了传输的正确性和顺序性 - 数据报socket (SOCK_DGRAM)
定义了一种无连接的服务。数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠的、无差错的。UDP是此类型的socket - 原始socket (SOCKET_RAW) 用于新的网络协议实现的测试等
允许对底层协议例如IP或ICMP进行直接访问,功能比较强大,但是使用不方法,一般用于协议的开发。
- 流式socket (SOCK_STREAM)
-
1.4 socket的信息数据结构
-
socket使用结构体sockaddr来存储domain、IP地址和端口等信息。
struct sockaddr{
unsigned short sa_family; //存放domain,使用的协议
char sa_data[14]; //存放14字节的协议地址,保存ip和端口号
}
- 由于sockaddr 是一个通用的结构体,ip地址和端口号都需要记录在sa_data中,使用起来不态方便。不同的socket_domain有自己不同的记录地址的结构体,例如IPv4的为 sockaddr_in
#include <netinet/in.h>
struct sockaddr_in{
short int sin_family; //地址组
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8];//填充0以确保和sockaddr结构体大小一致
}
struct in_addr{
in_addr_t s_addr; //32位ipv4地址,网络字节序
}
2.大端模式和小端模式
- 计算机数据存储有两种字节优先顺序:
- 高位字节优先 -> 大端法,内存的高地址存储数据的低位,低地址存储数据的高位。
- 低位字节优先 -> 小端法,内存的高地址存储数据的高位,低地址存储数据的地位。
//检验机器是大端法还是小端法的方法
long num = 0x12345678;
char* p = (char*)#
for(int i = 0;i<sizeof(long);++i){
printf("address:%p\t value:%x\n",p,*p++);
}
- 系统主机采用的字节序可能是小端法也可能是大端法。而网络通信则固定使用的是大端法。
因此进行网络通信绑定socket地址、端口号时需要使用主机字节序列向网络字节的转换。 - 地址格式的转换
#include<arpa/inet.h>
/*
h代表host,n代表net,
s代表 short类型,用以表示端口号
l代表long类型,用以表示ip地址
*/
//-**主机转向网络**
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
//-**网络转向主机**
uint_16 ntohs(uint16_t netshort);
uint_32 ntohl(uint32_t netlong);
- 将用点分十进制表示的ipv4地址字符串转换成socket编程需要的32位2进制数值地址。
// 1.ipv4和ipv6通用的转换函数
//convert IPv4 and IPv6 addresses from text to binary form
int inet_pton(int af,const char* src.void* dst);//1.fa表示协议 2.src表示字符串来源 3.dst表示转换后存储的地址,一般用struct in_addr*类型
//convert IPv4 and IPv6 addresses from binary to text form
const char* inet_ntop(int af,const void* src,char* dst,socklen_t size);//1.fa指定协议 2.src一般传入需要转换的sturct in_addr* 3.dst 存储转换后的结果 4.size用来指定dst的大小
//2. ipv4专用函数 相对通用转换函数来说,就是减少了填写协议的步骤
in_addr_t inet_addr(const char *cp);
int inet_aton(const char* cp,struct in_addr *inp);
char* inet_ntoa(struct in_addr in);
- 域名地址的转换
将例如www.baidu.com之类的域名或主机名转换成ip地址
#include <netdb.h>
//存储域名、ip地址等相关信息的基本单位host entity->struct hostent
struct houstent{
char* h_name; //正式的主机名
char** h_aliases; //主机别名列表
int h_addrtype; //主机的ip地址类型
int h_length; //主机ip地址的长度
char** h_addr_list;//主机ip地址的列表
}
#define h_addr h_addr_list[0] //h_addr_list的第一个ip地址,为了保证兼容性,而做的设置
//1.通过域名获取ip等信息
struct hostent *gethostbyname(const char* name);
//2.通过ip地址获取域名
//需要传入1.struct in_addr*类型的ip地址 2.ip地址长度信息 3.协议类型
struct hostent* gethostbyaddr(const void*addr,socklen_t len,int type);
3.TCP通信
0.socket通信使用的接口函数
//1.创建一个套接字实例 socket()
//返回一个套接字文件描述符。
//1.domain用来指定套接字通信领域(协议) 2.type用来指定套接字的类型SOCK_STREAM或SOCK_DGRAM等等 3.protocol 用以指定socket需要使用的特殊的协议,一般填0即可
int socket(int domain, int type, int protocol);
//2.将socket绑定到地址 bind()
//1.sockfd传入使用的socket文件描述符 2.addr 需要提前设置好相应的结构体addr 3.size表示addr地址的长度
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//3.监听接入连接 listen()
//1.sockfd服务器的socket文件描述符 2.最大的等待连接的socket的数量
int listen(int sockfd, int backlog);
//4.接收连接 accept()
//返回一个新建的socke的文件描述符
//1.服务器的socket文件描述符 2.addr会传回连接端的地址信息,3.addrlen在使用之前需要初始化为addr需要使用的缓冲区的大小,之后会传回实际被复制进入缓冲区的数据的字节数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//5.连接到对等的socket connect()
//将文件描述符sockfd引用的主动socket,连接到addr和addrlen指定的监听socket上
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//6.读写
//也可以使用read()/write()文件描述符进行读写
//读
//不用用于数据报通信
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//可以用于数据报通信
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//写
//不可用于数据报通信
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//可用于数据报通信
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//7.连接终止 shutdown()
//也可以使用close()关闭socket的文件描述符来进行关闭socket操作
int shutdown(int socket, int how);
1.通信流程概览
2.服务器编程
#define IP 192.168.110.10
#define PORT 9999
#define N 200
int main(void){
//1.建立通信用套接字
int serfd = socket(AF_INET,SOCK_STREAM,0);
//2.建立结构体存储服务器地址
struct sockaddr_in seraddr;
//2.1 端口数据转换
seraddr.sin_port = htons(PORT);
//2.2 IP转换
seraddr.sin_addr.s_addr = inet_addr(IP);
//2.3绑定协议
seraddr.sin_family = AF_INET;
//3.服务器绑定
int ret = bind(serfd,(struct sockaddr*)&seraddr,sizeof(seraddr));
//4.进入监听状态
ret = listen(serfd,10);
//5.接收客户端连接
struct sockaddr_in newaddr;
socklen_t len = sizeof(newaddr);
int newSock = accept(serfd,(struct sockaddr*)&newSock,&len);
//6.发送数据
char msg[100] = {0};
strcpy(msg,"hello,world");
ret = send(newSock,msg,strlen(msg),0);
//7.读取数据
memset(msg,0,100);
ret = recv(newSock,msg,100,0);
if(0==ret) printf("connection failed");;
printf("%s\n",msg);
//8.关闭连接
close(newSock);
close(serfd);
return 0;
}
3.客户端编程
#define IP 192.168.110.10
#define PORT 9999
#define N 100
int main(void){
//1.建立通信套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
//2.建立结构体存储服务器的地址
struct sockaddr_in seraddr;
//2.1添加协议
seraddr.sin_family = AF_INET;
//2.2添加IP地址
seraddr.sin_addr.s_addr = inet_addr(IP);
//2.3添加端口号
seraddr.sin_port = htons(PORT);
//3.建立连接
int ret = connect(sockfd,(struct sockaddr*)&seraddr,sizeof(seraddr));
//4.发送数据
ret = write(sockfd,"hello,world",11);
//5.接收数据
char msg[100] = {0};
ret = read(sockfd,msg,100);
if(0==ret) printf("connection failed");
//6.关闭连接
close(sockfd);
return 0;
}
4.UDP通信
1.UDP通信流程
2.服务器编程
- 因为UDP是面向无连接的协议,因此没有指定通信连接的对象。也没有相应的监听函数
所有需要先公布自己的IP和端口号。等到通信对象给自己发消息后,使用recvfrom获取到对方的地址后才可以进行通信 - 控制不了自己从何处接收,但可以决定发往何处
#define IP 192.168.110.10
#define PORT 9999
int main(void){
//1.定义通信socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
//2.定义结构体绑定自己的端口号 /防止系统随机分配
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = inet_addr(IP);
//3.绑定
int ret = bind(sockfd,(struct sockaddr*)&sockaddr,sizeof(sockadr));
//4.定义结构体接收通信对象地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
//5.接收数据
char msg[100] = {0};
//会将接收到的对象的地址存储到peeraddr中,后面可以利用此地址发送数据
ret = recvfrom(sockfd,msg,100,0,(struct sockaddr*)&peeraddr,&peerlen);
//6.发送数据
ret =sendto(sockfd,"hello,world",11,(struct sockaddr*)&peeraddr,len);
//7.关闭socket
close(sockfd);
return 0;
}
3.客户端编程
客户端编程比较随意,只需要直到通信对象的IP地址和端口号即可
#define IP 192.168.110.10
#define PORT 9999
int main(void){
//1.建立通信套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
//2.建立结构体存储通信对象的地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
peeraddr.sin_family = AF_INET;
peeraddr.sin_port = htons(PORT);
peeradr.sin_addr.s_addr = inet_addr(IP);
//3.发送数据
int ret = sendto(sockfd,"hello,world",11,0,(struct sockaddr*)&peeraddr,peerlen);
//4.接收数据
char msg[100] = {0};
ret = recvfrom(sockfd,msg,100,0,(struct sockaddr*)&peeraddr,&peerlen);
//5.关闭套接字
close(sockfd);
return 0;
}
5.套接字其他功能
- 1.套接字选项
套接字选项能影响到套接字的多个功能。可以通过系统调用getsockopt()和setsockopt()来获取和设定套接字的选项。- setsockopt 要在bind之间使用才有效
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
sockfd :套接字文件描述符
level :选项定义的层次,可以设定SOL_SOCKET,IPPROTO_TCP,IPPROTO_IP和IPPROTO_IPV6集中,
optname:需设置的选项 根据level设定数值的不同,optname中可设置的选型也不同
optval:指针,指向存放选项设定的值的地址
optlen:optval缓冲区的长度
- level及其对应选项
SOL_SOCKET:在socket API层次上进行设置
optname | 说明 | 设定值类型 |
---|---|---|
SO_BROADCAST | 允许发送广播数据,UDP使用 | int |
SO_DEBUG | 允许调试 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_RCVBUF | 设定接收缓冲区大小 | int |
SO_SNDBUF | 设定发送缓冲区大小 | int |
SO_RCVLOWAT | 设定接收缓冲区下限值 | int |
SO_SNDLOWAT | 设定发送缓冲区下限值 | int |
SO_RCVTIMEO | 设定接收超时时间 | struct timeval |
SO_SNDTIMEO | 设定发送超时时间 | struct timeval |
SO_REUSEADDR | 允许重用本地地址和端口号 | int |
- 2 sendmsg和recvmsg
- 3.通过fcntl设置套接字模式