** 重点**
``
应用层的数据放入内核发送缓冲区,然后TCP模块调用IP的服务。这些工作对于用户来说,只用socket中的send()就可以完成
这点可以联系第三章的TCP服务特点
主机向自己所以的网络广播一个ARP请求,请求包含目的机器的网络地址,请求的机器会回应一个ARP应答,包含自己的MAC地址
下面是我自己通过tcpdump抓的
ARP, Request who-has 192.168.129.2 tell 192.168.129.131, length 28
ARP, Reply 192.168.129.2 is-at 00:50:56:fe:26:ee, length 46
tcpdump -i eth33 -nt -s 500 port domain
另一个终端
kwet@kwet-virtual-machine:~$ host -t A www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 182.61.200.7
www.a.shifen.com has address 182.61.200.6
ip协议的上层TCP协议是可靠连接的,但是ip协议是无状态(数据包之间独立),不可靠(路由丢弃等情况),无连接(不维持双方信息)
如果ip数据包超过了MTU(帧的最大数据传输单元)就要切片,切开以后也会添加ip头部信息
sudo tcpdump -ntv -i ens33 icmp
IP (tos 0x0, ttl 64, id 857, offset 0, flags [+], proto ICMP (1), length 1500)
192.168.129.131 > 192.168.129.133: ICMP echo request, id 50989, seq 1, length 1480
IP (tos 0x0, ttl 64, id 857, offset 1480, flags [none], proto ICMP (1), length 21)
192.168.129.131 > 192.168.129.133: ip-proto-1
length 的结果与书上一致
这里应该写ip路由,转发相关。这里就先不写,毕竟先关注的不是这块
必须双方先建立连接才能开始数据的读写,必须是双全工的(双方数据读写可以通过一个连接进行),完成数据交换后,必须断开连接释放资源。
tcp执行写操作将数据放入tcp发送缓冲区中,发送缓冲区数据可能被封装成一个或多个tcp报文段发出。接收端收到tcp报文段后,按照tcp报文段的序号,依次放入tcp接收缓冲区中,然后通知应用程序读取数据,接收端应用程序可以一次性将接收缓冲区的数据读出,也可以分多次读出
没想到读取操作,拷贝了这么多次,在计算机领域肯定有好的解决办法,见第6章
字节流概念:发送写操作和接受读操作没有任何数量关系,数据没有边界
tcp可靠传输,采用应答机制,超时重传(发送端会有一个定时器)。
建立
IP 192.168.129.133.54148 > 192.168.129.131.23: Flags [S], seq 4177390328, win 64240, options [mss 1460,sackOK,TS val 3553051023 ecr 0,nop,wscale 7], length 0
IP 192.168.129.131.23 > 192.168.129.133.54148: Flags [S.], seq 3190426506, ack 4177390329, win 65160, options [mss 1460,sackOK,TS val 49740699 ecr 3553051023,nop,wscale 7], length 0
IP 192.168.129.133.54148 > 192.168.129.131.23: Flags [.], ack 1, win 502, options [nop,nop,TS val 3553051024 ecr 49740699], length 0
关闭
192.168.129.133是客户端
我这里显示把报文5和报文6一起发送了,没有书上报文5的内容。报文5的内容就是一个简单的对报文4的ack
IP 192.168.129.133.54148 > 192.168.129.131.23: Flags [F.], seq 147, ack 106, win 502, options [nop,nop,TS val 3553060918 ecr 49740708], length 0
IP 192.168.129.131.23 > 192.168.129.133.54148: Flags [F.], seq 106, ack 148, win 509, options [nop,nop,TS val 49750594 ecr 3553060918], length 0
IP 192.168.129.133.54148 > 192.168.129.131.23: Flags [.], ack 107, win 502, options [nop,nop,TS val 3553060918 ecr 49750594], length 0
本端结束了发送,但是可以接收数据。如果本端关闭,对方再去read就会返回0,这样应用程序就知道对方关闭连接
一端挂了,一端还在保持连接,即使挂了的那端重启,重新连接, 重启的那端会回复 复位报文段,告诉对方关闭连接或者重新连接。
这个状态要保持2MSL时间。建议是2min,这个状态处于报文7。
目的:
cookie是服务器发送给客户端的特殊信息,(响应头部set-cookie),客户端每次向服务器发送请求的时候都要带上这些信息,通过(请求头部cookie)这样服务器就可以区分不同客户
//网络通信时,需要将主机字节序转换成网络字节序(大端),
//另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 -》 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 《- 网络字节序
// 转IP,一般不用!!
uint32_t htonl(uint32_t hostlong); // 主机字节序 -》 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 《- 网络字节序
// 一般我们传入的参数ip地址和端口都是字符串形式
atoi(const char *s)//把字符串转成整型,用于端口,ip地址不会使用
//ip地址转成网络字节序
#include
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
#include
#include
#include // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 一般是5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
int close(int fd)
- 功能:关闭socketfd,使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。
int shutdown(int sockfd, int how)
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后是SHUT_WR。
shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。
2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。
但如果一个进程 close(sfd) 将不会影响到其它进程。
3. shutdown与socket描述符没有关系,即使调用shutdown(fd, SHUT_RDWR)也不会关闭fd,最终还需close(fd)。
close和shutdown具体参考:
https://blog.csdn.net/u011721450/article/details/107484707?spm=1001.2101.3001.6650.11&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-11.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-11.pc_relevant_default&utm_relevant_index=16
//普通文件读写
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
//socket读写
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
//分散读,写
#include
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base;//缓冲区的起始地址
size_t iov_len;//缓冲区的大小
}
- iov: 指向iovec结构体的指针
- iovcnt: iovec结构体对象的数目
- return: 已读或已写的字节数,出错返回-1
更多参考:https://blog.csdn.net/a3876247995/article/details/123596858
//端口复用
setsockopt()//传入SO_REUSEADDR
//优雅关闭
setsockopt()//传入SO_LINGER
#include
int pipe(int fd[2])//创建管道,fd[0]读端,fd[1]写端,默认管道大小65536字节
int dup(int old_fd)//创建一个新的fd,新的fd与old_fd指向相同的文件,管道,网络连接,但不继承old_fd的文件属性(noblock等)
int fcntl(int fd,int cmd,...)//对文件描述符进行操作,cmd可以去查手册
//一般用于设置非阻塞
int setnoblock(int fd){
int old_option=fcntl(fd,F_GETEL);//获取旧的文件描述符状态
int new_option=old_option | O_NOBLOCK;//设置非阻塞
fcntl(fd,F_SETFL,new_option);
return old_option;//返回旧的文件描述符
}
写数据过程:
#include
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
//它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,
//返回值是实际复制数据的长度,完全在内核操作,用于两个文件描述符之间
#include
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);//两个文件描述符之间移动数据
fd_in参数是待输入描述符。如果它是一个管道文件描述符,则off_in必须设置为NULL;
否则off_in表示从输入数据流的何处开始读取,此时若为NULL,则从输入数据流的当前偏移位置读入。
fd_out/off_out与上述相同,不过是用于输出。
len参数指定移动数据的长度。
flags参数则控制数据如何移动:
- SPLICE_F_NONBLOCK:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 I/O ,那么调用 splice 有可能仍然被阻塞。
- SPLICE_F_MORE:告知操作系统内核下一个 splice 系统调用将会有更多的数据传来。
- SPLICE_F_MOVE:如果输出是文件,这个值则会使得操作系统内核尝试从输入管道缓冲区直接将数据读入到输出地址空间,这个数据传输过程没有任何数据拷贝操作发生。内核2.6以后没有用了
- 使用splice时, fd_in和fd_out中必须至少有一个是管道文件描述符。
调用成功时返回移动的字节数量;它可能返回0,表示没有数据需要移动,这通常发生在从管道中读数据时而该管道没有被写入的时候。
两个管道之间复制数据!这是复制
#include
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);//两个管道之间复制数据!这是复制,源文件的数据依然可以操作
#include
#include
#include
#include
#include
#include
#include
bool daemonize()
{
pid_t pid = fork();
if (pid < 0) {
return false;
}
else if (pid > 0) {
exit(0);//关闭父进程
}
umask(0);//设置文件权限
//设置新的会话,设置本进程为进程组的首领
pid_t sid = setsid();
if (sid < 0)
return false;
//切换工作目录
if ((chdir("/")) < 0)
return false;
printf("2. pid: %ld, parent id: %ld\n", (long)getpid(), (long)getppid());
//关闭标准输入,标准输出,标准错误输出
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//重定向标准输入,标准输出,标准错误输出到/dev/null
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
return true;
}
##相同功能的库函数
int daemon(int nochdir int noclose)
I/O处理单元是服务器管理客户端连接的模块,通常的工作:等待并接受新的客户连接,接受客户数据,将服务器响应数据返回给客户端。数据的收发有可能是在逻辑单元中执行。
一个逻辑单元通常是一个进程或线程,分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端。一般都是解析客户端发来的请求,并且把资源响应给客户端
网络存储单元可以是数据库、缓冲和文件,甚至是一台独立的服务器,但不是必须的,比如ssh、telnet等登陆服务就不需要这个单元
请求队列是个单元之间的通信方式的抽象,I/O处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞争条件。
记住服务器基本都是非阻塞I/O,阻塞的话就白白浪费cpu资源了
非阻塞I/O执行系统调用立即返回,不管时间是否已经发生。如果事件没有立即发生,这些系统调用返回-1,和不错的情况一样,必须根据errno来区分两种情况,对accept、send、recv而言,事件未发生errno通常被设置为EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期望阻塞”);对connect而言,errno被设置成EINPROGRESS(意为“在处理中”)
说人话,就是我们一般要忽略errno为EAGAIN或者EWOULDBLOCK的情况,所以要对read()的返回结果进行判断
I/O复用是常用的I/O通知机制,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。I/O复用函数本身是阻塞的,它具有同时监听多个I/O事件的能力来提高效率。
异步IO不讨论
Reactor 模式要求 主线程(I/O 处理单元) 只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何其他实质性的工作。 读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步 I/O 模型(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
主线程调用 epoll_wait 等待 socket 上有数据可读。
当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
主线程调用 epoll_wait 等待 socket 可写。
当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
睡眠在请求队列上的某个工作进程被唤醒,它往 socket 上写入服务器处理客户请求的结果。
工作线程从请求队列中取出事件后,将根据事件类型来决定如何处理它:对于可读事件,执行读数据和处理请求的操作;对于可写事件,执行写数据的操作。因此,Reactor 模式中没必要区分所谓的 “读工作线程” 和 “写工作线程”。
工作线程去读取和写入socket数据
与 Reactor 模式不同,Proactor 模式 将所有 I/O 操作都交给主线程和内核来处理, 工作线程仅仅负责业务逻辑。
使用异步 I/O 模型(以 aio_read 和 aio_write为例)实现的 Proactor 模式的工作流程是:
主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例)
主线程继续处理其他逻辑。
当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以 通知应用程序数据已经可用。
应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写
操作完成时如何通知应用程序(仍然以信号为例)
主线程继续处理其他逻辑
当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
连接 socket 上的读写事件是通过 aio_read/aio_write 向内核注册的,因此内核将通过信号来向应用程序报告连接 socket 上的读写事件。所以,主线程中的 epoll_wait 调用仅能用来检测监听 socket 上的连接请求事件,而不能用来检测 socket 上的读写事件。
sokcet的数据读写是由内核去完成
使用同步I/O方式模拟出Proactor模式的原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。
使用同步I/O模型(仍以epoll_wait为例)模拟出Proactor模式的工作流程如下:
主线程往epoll内核事件表中注册socket上的读就绪事件。
主线程调用epoll_wait等待socket上有数据可读。
当socket上有数据可读时,epoll_wait 通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。
当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
简单来说,就是主线程来对socket读写,工作线程只负责业务处理
一般都是Reactor模式和模拟Procactor模式
1.半同步半异步模式
半同步半异步模式的变体——半同步半反应堆模式采用的事件处理模式是Reactor
2.领导者/追随者模式
多个线程轮流获得事件资源集合,轮流监听IO事件,分发并处理事件
应用在解析http请求
while(case!=c)
{
case a:
fun();
case=b;//转化状态
break;
case b:
fun();
case=c;//转换状态
break;
}
IO复用作用是监听多个文件描述符
只能LT,O(n)复杂度
只能LT,O(n)复杂度
LT和ET都可以,O(1)复杂度
while(1){
int ret=read();
if(ret<0)
{
if(errno==EAGAIN)
{
printf("读完了/n");
break;
}
close();
break;
}else if(ret==0){
clsoe();
}else{
printf("读到了");
}
}
epoll模式中事件可能被触发多次,比如socket接收到数据交给一个线程处理数据,在数据没有处理完之前又有新数据达到触发了事件,另一个线程被激活获得该socket,从而产生多个线程操作同一socket,即使在ET模式下也有可能出现这种情况。采用EPOLLONETSHOT事件的文件描述符上的注册事件只触发一次,要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态。
只能对连接文件描述符注册EPOLLONESHOT,如果对监听文件描述符设置EPOLLONESHOT事件,那么应用程序只能处理一个客户端连接,因为后续的连接请求不会触发EPOLLIN事件
当一个管道读的一段关闭了或者socke连接一端已经关闭,再写数据,就会触发SIGPIPE信号。
该信号的缺省学位是终止进程,因此进程必须忽略或者捕获它,以免被终止。并且可以通过erro等于epipe的返回知道连接关闭