一、网络基础
1.1 协议
就是一种规则
1.2 典型协议
应用层 常见的协议有HTTP协议,FTP协议。
传输层 常见协议有TCP/UDP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层(链路层) 常见协议有ARP协议、RARP协议。
HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
IP协议是因特网互联协议(Internet Protocol)
ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。
1.3 分层模型结构
1、OSI七层模型:物、数、网、传、会、表、应
2、TCP/IP四层模型:网(链路层/网络接口层)、网、传、应
(1)物理层:硬件部分,为上层协议提供了一个传输数据的可靠物理媒体。
(2)数据链路层:把网络层传下来的数据报组装成帧。数据链路层的传输单位是帧。
(3)网络层:对数据包进行路由选择,建立连接、保持和终止连接。(IP)
(4)传输层:负责不同主机中两个进程(每个进程都有端口号)的通信,即端到端(两个端口之间)的通信。传输单位是报文段或用户数据报。(TCP/UDP)
(5)会话层:向表示层实体/用户进程提供建立连接并在连接上有序的传输数据。这是会话,也是建立同步。
(6)表示层:对上层信息进行变化,以保证一个主机应用层信息可以被另一个主机的应用层程序理解。
(7)应用层:为操作系统或网络应用程序提供访问网络服务的接口。
1.4 通信过程
两台计算机通过TCP/IP协议通讯的过程如下所示:
1.5 网络传输流程:
数据在没有封装前是不能进行传递的,其实就是不知道目的地址。
需要先后经过应用层、传输层、网络层和链路层的封装后才能进行传输。我们需要做的是应用层的封装,后面三层的封装是内核完成的。
1.6 IP协议
版本:IPv4、IPv6
TTL:time to live 设置数据包在路由节点中的跳转上限,每经过一个路由节点该值-1,减为0的路由,有义务将该数据包丢弃。
源IP:32位—4字节。 192.168.1.108(点分十进制,人看的,string类型)进入网络后会转成二进制。
目的IP:32位—4字节。
IP地址:可以在网络环境中,唯一标识一台主机。
1.7 UDP协议
16位:源端口号 2^16 = 65536 (端口号最大只能取到65535.)
16位:目的端口号
端口号:可以在一台主机上唯一标识一个进程。
IP地址+端口号:可以在网络环境中唯一标识一个进程。
1.8 TCP协议
16位:源端口号 2^16 = 65536。
16位:目的端口号。
32位序号
32位确认序号
6个标志位
16位窗口大小
三次握手是由内核完成的,在用户端就是accept()和connect()函数成功执行并返回了。
1.9 网络应用层序设计模式
C/S模式:client-server模式,客户机-服务器模式。
B/S模式:browser-server模式,浏览器-服务器模式。
优缺点 | C/S | B/S |
---|---|---|
优点 | 缓存大量数据(端游)、协议选择更灵活、速度快 | 安全性、跨平台、开发工作量小 |
缺点 | 安全性、不能跨平台、开发工作量大 | 不能缓存大量数据(页游)、必须严格遵守http协议 |
二、socket编程
2.1 预备知识
在通信过程中套接字一定是成对出现的。一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。
网络字节序:网络数据流采用大端字节序(高位在低地址,低位在高地址),但是计算机采用的是小端法,因此需要进行网络字节序和主机字节序的转换。
常用的四个转换函数:
htonl() 本地-->网络(IP)这里不能用点分十进制写法,因为它的本质是字符串
htons() 本地-->网络(Port)
ntohl() 网络-->本地(IP)
ntohs() 网络-->本地(Port)
IP地址转换函数:
int inet_pton(int af, const char* src, void* dst); 本地字节序(string IP) --> 网络字节序
af:AF_INET(IPv4)、AF_INET6(IPv6)
src:传入IP地址(点分十进制)
dst:传出转换后的网络字节序的IP地址
返回值:
成功:1
异常:0,说明src指向的不是一个有效的ip地址
失败:-1
const char* inet_ntop(int af, const void* src, char* dst socklen_t size); 网络字节序 --> 本地字节序(string IP)
af:AF_INET(IPv4)、AF_INET6(IPv6)
src:网络字节序IP地址
dst:本地字节序(string IP)
size:dst的大小
返回值:
成功:dst
失败:NULLL
2.2 socket的分类
socket提供了流(stream)和数据报(datagram)两种通信机制,即流socket和数据报socket。
流socket基于TCP协议,是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。
数据报socket基于UDP协议,不需要建立和维持链接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但它的效率比较高。
2.3 socket函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol); //创建一个套接字
domain:AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX
type:数据传输方式(套接字类型)SOCK_STREAM、SOCK_DGRAM
protocol:传输协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP传输协议和UDP传输协议。
传0表示系统会自动推演出使用什么协议,因为当使用SOCK_STREAM格式的套接字,必须使用TCP协议;反之必须使用UDP协议
返回值:
成功:新套接字所对应的文件描述符
失败:-1
int bind(Int sockfd, const struct sockaddr* addr, socklen_t addrlen); //给socket绑定一个地址结构(IP+port)
sockfd:socket函数返回值
addr的初始化:
#include <arpa/inet.h>
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = hton(INADDR_ANY);
addr:(struct sockaddr* )&addr
addrlen:sizeof(addr)地址结构的大小
返回值:
成功:0
失败:-1
int listen(int sockfd, int backlog); //设置同时与服务器建立连接的上限数值(同时进行三次握手的客户端数量)
sockfd:socket函数返回值
backlog:上限数值,最大128
返回值:
成功:0
失败:-1
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); //阻塞等待客户端建立链接,成功的话返回一个与客户端成功链接的socket文件描述符
sockfd:socket函数返回值
addr:传出参数,成功与服务器建立链接的那个客户端的地址结构(IP+port)
addrlen:传入传出。入:addr的大小。出:客户端addr的实际大小。
返回值:
成功:能与服务器进行数据通信的socket对应的文件描述符
失败:-1
Int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen); //使用现有的socket与服务器建立链接
sockfd:socket函数返回值
addr初始化:
struct sockaddr_in srv_addr; //服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527; //跟服务器bind时设定的port完全一致
inet_pton(AF_INET, "服务器的IP地址", &srv_addr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1
如果不使用bind绑定客户端地址结构,采用”隐式绑定“
2.4 TCP通信流程分析
server:
1.socket() 创建socket
2.bind() 绑定服务器地址结构
3.listen() 设置监听上限
4.accept() 阻塞监听客户端链接
5.read() 读socket获取客户端数据
6.小写-->大写 toupper()
7.write()
8.close()
client:
1.socket() 创建socket
2.connect() 与服务器建立链接
3.write() 写数据到socket
4.read() 读转换后的数据
5.显示读取结果
6.close()
三、多进程、多线程并发服务器
3.1 多进程并发服务器
多进程并发服务器思路分析
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accpet(); 接收客户端连接请求。
pid = fork();
if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd)
close(lfd) 关闭用于建立连接的套接字 lfd
read()
小--大
write()
} else if (pid > 0) {
close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
}
}
5. 子进程:
close(lfd)
read()
小--大
write()
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中, 完成子进程回收
while (waitpid());
3.2 多线程并发服务器
多线程并发服务器思路分析
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accept(lfd, );
pthread_create(&tid, NULL, tfn, (void *)cfd);
pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。
}
5. 子线程:
void *tfn(void *arg)
{
// close(lfd) 不能关闭。 主线程要使用lfd
read(cfd)
小--大
write(cfd)
pthread_exit((void *)10);
}
3.3、select
3.3.1 select函数原型
select多路IO转接:
原理: 借助内核用select 来监听:客户端连接、数据通信事件。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个:读、写、异常)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
3.3.2 select相关参数
void FD_CLR(int fd, fd_set *set) 把某一个fd清除出去
int FD_ISSET(int fd, fd_set *set) 判定某个fd是否在位图中
void FD_SET(int fd, fd_set *set) 把某一个fd添加到位图
void FD_ZERO(fd_set *set) 位图所有二进制位置零
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
3.3.3 select实现多路IO转接设计思路
思路分析:
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) { 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
}
for (i = lfd+1; i <= 最大文件描述符; i++){
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
}
}
}
3.3.4 select实现多路IO转接-代码
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <arpa/inet.h>
6. #include <ctype.h>
7.
8. #include "wrap.h"
9.
10. #define SERV_PORT 6666
11.
12. int main(int argc, char *argv[])
13. {
14. int i, j, n, nready;
15.
16. int maxfd = 0;
17.
18. int listenfd, connfd;
19.
20. char buf[BUFSIZ]; /* #define INET_ADDRSTRLEN 16 */
21.
22. struct sockaddr_in clie_addr, serv_addr;
23. socklen_t clie_addr_len;
24.
25. listenfd = Socket(AF_INET, SOCK_STREAM, 0);
26. int opt = 1;
27. setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
28. bzero(&serv_addr, sizeof(serv_addr));
29. serv_addr.sin_family= AF_INET;
30. serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
31. serv_addr.sin_port= htons(SERV_PORT);
32. Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
33. Listen(listenfd, 128);
34.
35.
36. fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
37.
38. maxfd = listenfd;
39.
40. FD_ZERO(&allset);
41. FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
42.
43. while (1) {
44. rset = allset; /* 每次循环时都从新设置select监控信号集 */
45. nready = select(maxfd+1, &rset, NULL, NULL, NULL);
46. if (nready < 0)
47. perr_exit("select error");
48.
49. if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
50.
51. clie_addr_len = sizeof(clie_addr);
52. connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
53.
54. FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
55.
56. if (maxfd < connfd)
57. maxfd = connfd;
58.
59. if (0 == --nready) /* 只有listenfd有事件, 后续的 for 不需执行 */
60. continue;
61. }
62.
63. for (i = listenfd+1; i <= maxfd; i++) { /* 检测哪个clients 有数据就绪 */
64.
65. if (FD_ISSET(i, &rset)) {
66.
67. if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
68. Close(i);
69. FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */
70.
71. } else if (n > 0) {
72.
73. for (j = 0; j < n; j++)
74. buf[j] = toupper(buf[j]);
75. Write(i, buf, n);
76. }
77. }
78. }
79. }
80.
81. Close(listenfd);
82.
83. return 0;
84. }
编译运行,结果如下:
如图,借助select也可以实现多线程
3.3.5 select优缺点
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
3.4 poll
poll是对select的改进,但是它是个半成品,相对select提升不大。最终版本是epoll,所以poll了解一下就完事儿,重点掌握epoll。
3.4.1 POLL函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】
struct pollfd {
int fd: 待监听的文件描述符
short events: 待监听的文件描述符对应的监听事件。取值:POLLIN、POLLOUT、POLLERR
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
3.4.2 poll函数实现服务器
1. /* server.c */
2. #include <stdio.h>
3. #include <stdlib.h>
4. #include <string.h>
5. #include <netinet/in.h>
6. #include <arpa/inet.h>
7. #include <poll.h>
8. #include <errno.h>
9. #include "wrap.h"
10.
11. #define MAXLINE 80
12. #define SERV_PORT 6666
13. #define OPEN_MAX 1024
14.
15. int main(int argc, char *argv[])
16. {
17. int i, j, maxi, listenfd, connfd, sockfd;
18. int nready;
19. ssize_t n;
20. char buf[MAXLINE], str[INET_ADDRSTRLEN];
21. socklen_t clilen;
22. struct pollfd client[OPEN_MAX];
23. struct sockaddr_in cliaddr, servaddr;
24.
25. listenfd = Socket(AF_INET, SOCK_STREAM, 0);
26.
27. bzero(&servaddr, sizeof(servaddr));
28. servaddr.sin_family = AF_INET;
29. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
30. servaddr.sin_port = htons(SERV_PORT);
31.
32. Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
33.
34. Listen(listenfd, 20);
35.
36. client[0].fd = listenfd;
37. client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
38.
39. for (i = 1; i < OPEN_MAX; i++)
40. client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
41. maxi = 0; /* client[]数组有效元素中最大元素下标 */
42.
43. for ( ; ; ) {
44. nready = poll(client, maxi+1, -1); /* 阻塞 */
45. if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
46. clilen = sizeof(cliaddr);
47. connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
48. printf("received from %s at PORT %d\n",
49. inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
50. ntohs(cliaddr.sin_port));
51. for (i = 1; i < OPEN_MAX; i++) {
52. if (client[i].fd < 0) {
53. client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
54. break;
55. }
56. }
57.
58. if (i == OPEN_MAX)
59. perr_exit("too many clients");
60.
61. client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
62. if (i > maxi)
63. maxi = i; /* 更新client[]中最大元素下标 */
64. if (--nready <= 0)
65. continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
66. }
67. for (i = 1; i <= maxi; i++) { /* 检测client[] */
68. if ((sockfd = client[i].fd) < 0)
69. continue;
70. if (client[i].revents & (POLLRDNORM | POLLERR)) {
71. if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
72. if (errno == ECONNRESET) { /* 当收到 RST标志时 */
73. /* connection reset by client */
74. printf("client[%d] aborted connection\n", i);
75. Close(sockfd);
76. client[i].fd = -1;
77. } else {
78. perr_exit("read error");
79. }
80. } else if (n == 0) {
81. /* connection closed by client */
82. printf("client[%d] closed connection\n", i);
83. Close(sockfd);
84. client[i].fd = -1;
85. } else {
86. for (j = 0; j < n; j++)
87. buf[j] = toupper(buf[j]);
88. Writen(sockfd, buf, n);
89. }
90. if (--nready <= 0)
91. break; /* no more readable descriptors */
92. }
93. }
94. }
95. return 0;
96. }
3.4.3 poll函数总结
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
3.5 epoll
3.5.1 epoll函数原型
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd:
待监听的fd
event: 本质 struct epoll_event 结构体 地址
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd
void *ptr;
uint32_t u32;
uint64_t u64;
返回值:成功 0; 失败: -1 errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
epfd:epoll_create 函数的返回值。 epfd
events:传出参数,【数组】, 满足监听条件的 那些 fd 结构体。
maxevents:数组 元素的总个数。 1024
struct epoll_event evnets[1024]
timeout:
-1: 阻塞
0: 不阻塞
>0: 超时时间 (毫秒)
返回值:
> 0: 满足监听的 总个数。 可以用作循环上限。
0: 没有fd满足监听事件
-1:失败。 errno