一. TCP/IP协议
协议即: 通信双方必须遵循的规则, 由ISO规定, RFC 文档。
1. OSI 七层模型 与 TCP/IP 四层模型
2. TCP/IP 协议族的成员们
- 应用层(http 超文本传输协议 ftp 文件传输协议, telnet 远程登录协议, ssh 安全外壳协议, smtp 简单邮件发送协议, pop3 接收邮件协议)
- 传输层 (tcp 传输控制协议, udp 用户数据包协议)
- 网络层 (ip 网络互联协议, icmp 网络控制消息协议, igmp 网络组管理协议)
- 网络接口层(arp 地址转换协议, rarp 反向地址转换协议, mpls 多协议标签交换)
每一层负责的工作:
- 网络接口层: 负责将二进制流转换为数据帧,并进行数据帧的发送和接收, 数据帧是独立的网络信息传输单元。
- 网络层:负责将数据帧封装层IP数据报,并运行必要的路由算法。负责点到点的传输(主机或路由器)。
- 传输层: 负责端对端之间的通信会话连接和建立,传输协议的选择根据数据传输的方式而定,负责端到端的传输(源主机与目的主机)
- 应用层: 负责应用程序的网络访问,通过端口号来识别各个不同的进程。
3. ip协议与路由器
其中网络层的 ip 协议是构成 Internet 的基础, 互联网的主机通过IP地址来表示,且互联网上有大量路由器
负责根据IP地址选择合适的路径转发数据包。
路由器是在工作在网络层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包。
IP 协议不保证数据传输的可靠性,数据包在传输过程中可能丢失, 可靠性可用在上层协议或应用程序中提供支持。
4.每一层相关协议的注解
“IP地址和MAC地址相同点是它们都唯一,不同的特点主要有: 对于网络上的某一设备,如一台计算机或一台路由器,其IP地址是基于网络拓扑设计出的,同一台设备或计算机上,改动IP地址是很容易的(但必须唯一),而MAC则是生产厂商烧录好的,一般不能改动。
-
ARP: (地址转换协议)用于获得同一物理网络中的硬件主机地址(MAC),是设备通过知道的IP地址获得主机不知道的物理地址的协议。
-
RARP: (反向地址转换协议)允许局域网的物理机器从网关服务器的 ARP表或缓存上请求其IP地址。
-
IP :(网际互联协议)负责在主机和网络之间寻址和路由转发数据包。
-
ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
-
IGMP:(网络组管理协议)被 IP 主机用来向本地多路广播路由器报告主机组成员的的协议,主机与本地路由器之间使用 Internet
组管理协议来进行组播组成员信息的交互。 -
TCP:(传输控制协议)为用户提供可靠的通信连接。 适合于一次传输大批数据的情况,并适用于要求得到响应的应用程序。
-
UDP: (用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证,适用于一次传输少量数据。
二. 网络相关概念
1. socket 概念
Linux 中的网络编程是通过 socket 接口来进行的。
特点:
- socket 是一种特殊的I/O接口,也是一种文件描述符
- socket 是常用的进程间通信机制(同机器或非同机器之间均可)
组成:
- 每一个socket 都用一个半相关描述{ 协议, 本地地址,本地端口} 表示,完整的套接字使用一个 相关描述{协议,本地地址,本地端口, 目的地址,目的端口} 表示
- socket 类似于文件的函数调用,使用其返回一个socket描述符,随后的连接建立,数据传输等操作均通过 socket来实现。
2. socket 类型
(1) 流式 socket(SOCKET_STREAM) -> 用于 TCP 通信
(2) 数据报 socket(SOCKET_DGRAM) -> 用于 UDP通信
(3) 原始 socket(SOCKET_RAW)-> 用于新的网络协议实现的测试等
原始套接字允许对底层协议如IP和ICMP进行直接访问,功能强大但使用不便,主要用于一些协议的开发。
3. socket 信息数据结构
#include <netinet/in.h>
struct sockaddr {
unsigned short sa_family; //地址族
char sa_data[14]; //14字节的协议地址包含其socket的IP地址于端口号
};
struct sockaddr_in {
//在sockaddr上进行了划分,使得端口,ip分开(常用)
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8]; //填充的字节,与sockaddr保持一样大小
};
struct in_addr {
in_addr_t s_addr; //32位ipv4地址,网络字节序
}
sin_family: AF_INET-> IPV4 协议 AF_INET6 -> IPV6协议
4.数据存储优先顺序的转换
在计算机数据存储有两种字节优先顺序:高位字节优先(大端模式)和低位字节优先
- 内存的低地址存储数据的低字节,高地址存放数据的高字节的方式叫小端模式。
- 内存的高地址存储数据的低字节,低地址存放数据的高字节的方式叫大端模式
例如: 10 在 内存中存放的是 0x00000000a;
大端模式即: 0x00000000a
小端模式即: 0x0a0000000
可以用 char* 取一个int类型的变量的地址(用10),得到如果解引用后是10,那么是小端,是0则是大端
因为指针总是取一个变量的低地址开始(栈的地址是向下增长)
但是呢,在网络中的端口号和IP地址都是以网络字节序(大端法),因此我们需要对这两个变量的字节存储优先顺序进行相互转化。
这里用到4个函数:
#include <netinet/in.h>
uint16_t htons(unit16_t host16bit); 从host16位比特转换位 network 的short类型(用于端口转换)
uint16_t ntohs(unit16_t net16bit); 反转
unit32_t htonl(unit32_t host32bit); 从host32位比特转换为 network的long 类型(用于ip转换)
unit32_t ntohl(unit32_t net32bit); 反转
成功:返回转换的字节序
失败:返回-1
5. 地址格式转换
我们在表达地址时经常采用的是点分十进制表示的数值,而socket 编程中使用的是32位的网络字节序的二进制值
,这就需要对其两个数值进行转换。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *straddr); //将点分十进制转换为 32位二进制的网络字节序地址
char *inet_ntoa(struct in_addr inaddr); //将32位二进制网络字节序地址转换为点分十进制
三. Socket 编程
1. 使用TCP协议的流程图
服务器端:
1. 头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
2. socket 函数: 生成套接口描述符
int socket(int domain, int type, int protocol)
参数:
- domain { AF_INET: IPV4 协议, AF_INET4: IPV6 网络协议}
- type { SOCK_STREAM: TCP SOCK_DGRAM: UDP }
- protocol : 指定socket 所使用的传输协议编号, 通常为0
返回值: 成功返回套接口描述符,失败返回 -1;
演示:
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1) {
perror("socket");
exit(-1);
}
3. bind 函数:用来绑定一个端口号和 IP地址,使套接口与指定的端口号和IP地址相关联,
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
参数:
- sockfd : 为前面socket的返回值
- my_addr: 为结构体指针变量 (可使用 sockaddr_in 输入其信息,并强制转换为 sockaddr)
- addrlen : sockaddr 的结构体长度,通常是计算 sizeof(struct sockaddr);
返回: 成功返回0,失败返回 -1
演示:
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(struct sockaddr_in));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);
my_addr.sin_addr.s_addr = inet_addr("192.168.2.100")
if(bind(sfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);;
}
快捷方式:
- 通过将 my_addr.sin_port 置为 0,函数会自动选择未占用的端口使用,
- 通过将 my_addr.sin_addr.s_addr 置为 INADDR_ANY,系统会自动填入本机IP地址
4. listen 函数:使服务器的这个端口和 IP处于监听状态,等待网络中客户端的连接请求,如果
客户端有连接请求,端口就会接收这个连接。
int listen(int sockfd, int backlog);
参数:
- sockfd : 为socket的返回值,即 sfd
- backlg: 指定同时能处理的最大连接要求,通常为 10 或 5,最大值可设置 12(半连接队列的大小,第一次握手后将客户端syn加入)
返回值: 成功返回0,失败则返回 -1
常用实例:
if(listen(sfd, 10) == -1) {
perror("listen");
close(sfd);
exit(-1);
}
5. accept函数: 接收远程客户端的连接请求,并将该请求放于等待队列,闲暇时刻返回该 socket 标识符,用其可与
连接的客户端 socket 进行通信。
(当accept 返回时,返回的socket代表已经完成3次握手的客户端)
int accpet(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd 为前面 socket 的返回值,即 sfd
- addr:为结构体变量,和 bind 的结构体是同类型,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
- addrlen: 表示结构体的长度,为整型指针
返回值: 成功则返回新的 socket 处理代码 new_fd, 失败返回 -1
实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr *)&clientaddr, &addrlen);
if(new_fd == -1) {
perror("accept");
close(new_fd);
exit(-1);
}
6. recv 函数:用与客户端通信的socket来接收远端主机传来的数据,并把数据存到由参数buf指向的内存空间。
int recv(int sockfd, void *buff, int len, unsigned int flags);
参数:
- sockfd: 为前面accept的返回值,即 new_fd, 也就是新的套接字
- buf: 表示缓冲区
- len: 表示缓冲区的长度
- flags: 通常为 0
返回值: 成功返回实际接收到的字符数,可能少于你所指定的接收长度。 失败返回 -1
实例:
char buf[512] = {
0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1) {
perror("recv");
close(new_fd);
close(sfd);
exit(-1);
}
7. send函数: 用新的套接字发送数据给指定的远端主机
int send(int client_fd, const void *msg, int len, unsigned int flags);
参数:
- client_fd: 为accept 的返回值,
- msg:一般为常量字符串
- len:表示长度
- flags:通常为 0
返回值: 成功则返回实际传送出去的字符数, 可能少于你指定的发送长度,失败则返回 -1
常用实例:
if(send(new_fd, "hello", 6,0) == -1) {
perror("send");
close(new_fd);
close(sfd);
exit(-1);
}
8. close函数:当使用完文件后若已不再需要则可使用 close()关闭该文件,并且 close() 会让数据写回磁盘
int close(int fd);
参数:
- fd : 为前面的 sfd
返回值: 若文件顺利关闭返回 0, 发生错误时返回 -1
注意:close 的 fd, 遵循引用计数原则,dup机制
客户端:
- connect 函数: 用来连接远程服务器。
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
参数:
- sockfd : 为前面 socket 的返回值,即 sfd
- serv_addr: 为结构体指针变量,存放远程服务器的 ip 和端口信息
- addrlen: 表示结构体变量的长度
返回值: 成功则返回0,失败返回 -1
2. 设置套接口选项 setsockopt 的用法
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
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:需要设置的选项
- optval:指针,指向存放选项值的缓冲区
- optlen:optval 缓冲区长度
注意: 该设置全部必须要放在 bind 之前
1. 服务器关闭后象继续重用 socket,(正常是需要经历 TIME_WAIT 的过程)
int reuse = 1;
setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(int))
2. 在 send(), recv() 过程中有时由于网络状况等原因,发收不能预期进行,需要设置收发时限
int nNetTimeout = 1000; //1s
//发送时限
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&nNetTimeout, sizeof(int))
//接收时限
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&nNetTimeout, sizeof(int));
3. 设置缓冲区大小,避免 send(), 和 recv() 不断的循环收发
int nRecvBuf = 32*1024;
setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (const char *)&nRecvBuf, sizeof(int));
int nSendBuf = 32*1024;
setsockopt(socket, SOL_SOCKET, SO_SNDBUF, (const char *)&nSendBuf, sizeof(int));
4. 一般在发送UDP数据报的时候,希望 socket 发送的数据具有广播特性。
int bBroadcast = 1;
setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char *)&bBroadcast, sizeof(int));
3. socketpair 创建在该进程中线程间通信的 socket
4. 五种I/O编程模型
1. 同步与异步,阻塞与非阻塞
同步与异步在进行I/O操作是针对应用程序与内核的交互而言的。
- 同步过程中进程出发I/O操作会等待(阻塞)或轮询地去查看 I/O操作(内核中进行处理I/O)(非阻塞)是否完成。
- 异步过程中进程触发 I/O 操作以后,直接返回,做自己的事情, I/O操作由内核处理,完成后内核通知进程I/O进程完成
阻塞与非阻塞关注的是单个进程的执行状态。
- 阻塞:进程给cpu传达任务后,就一直等待cpu处理完成,然后才执行后面的操作
- 非阻塞: 进程给cpu传达任务后,继续进行后续操作,隔断时间询问之前的操作是否完成。
2. IO模型
1. 阻塞 I/O模型
2. 非阻塞I/O模型
3. IO 复用模型
4. 信号驱动 I/O 模型
5. 异步 IO 模型
5. Epoll 多路复用
epoll 是 select 和 poll 的增强版本,是实现 I/O多路复用的主要方式
1. 主要接口:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
2. 接口详解
1. int epoll_create(int size);
创建一个 epoll 的实例,返回实例的文件描述符, size参数已经没有意义,但需要大于0.
注意: epoll 创建后会占用一个fd, 使用完 epoll 后,必须调用 close() 关闭。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event (*)event)
epoll 的事件注册函数,第一个参数是 epoll 描述符, 第二个表示 动作,有三个宏
EPOLL_CTL_ADD: 注册新的fd 到 epfd 中
EPOLL_CTL_MOD: 修改已经注册的 fd 的监听事件
EPOLL_CTL_DEL: 从epfd 中删除一个fd。
第三个参数是需要监听的 fd, 第四个参数是告诉内核要监听什么事件,其结构是:
struct epoll_event {
__unit32_t events;
epoll_data_t data;
}
events 可以是下面几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断
EPOLLET: 将EPOLL设置为边缘触发(Edge Triggered)模式,相对于水平触发来说的
EPOLLONESHOT: 只监听一次事件,监听完后,就删除该描述符
水平触发与边缘触发
区别:
- LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处
理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。 - ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理
该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
选择ET:
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在
ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文
件描述符的任务饿死。
3. int epoll_wait(int epfd, struct epoll_event (*)events, int maxevents, int timeout);
等待事件的产生, 参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时
间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,
如返回 0 表示已超时。