硬件层面接收到报文之后,做一系列的初始化 *** 作,之后驱动才开始把一个封包封装为skb。
当然这是在x86架构下,如果是在cavium架构下,封包是wqe形式存在。
不管是skb还是wqe,都仅仅是一种手段,一种达到完成报文传输所采用的一种解决方案,一种方法而已。
或许处理方案的具体实现细节差别万千,但是基本的原理,都是殊途同归,万变不离其宗。
skb的产生,让Linux协议栈旅程的开启,具备了最基本的条件,接下来的协议栈之旅,才会更加精彩。
写作本文的原因是现在本机网络 IO 应用非常广。
在 php 中 一般 nginx 和 php-fpm 是通过 127.0.0.1 来进行通信的;
在微服务中,由于 side car 模式的应用,本机网络请求更是越来越多。
所以,如果能深度理解这个问题在各种网络通信应用的技术实践中将非常的有意义。
今天咱们就把 127.0.0.1 本机网络通信相关问题搞搞清楚!
为了方便讨论,我把这个问题拆分成3问:
1)127.0.0.1 本机网络 IO 需要经过网卡吗?
2)和外网网络通信相比,在内核收发流程上有啥差别?
3)使用 127.0.0.1 能比 192.168.x.x 更快吗?
在上面这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。
当数据包到达另外一台机器的时候,Linux 数据包的接收过程开始了。
当网卡收到数据以后,CPU发起一个中断,以通知 CPU 有数据到达。
当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。
ksoftirqd 检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。
当协议栈处理完并把数据放到接收队列的之后,唤醒用户进程(假设是阻塞方式)。
关于跨机网络通信的理解,可以通俗地用下面这张图来总结一下:
前面,我们看到了跨机时整个网络数据的发送过程 。
在本机网络 IO 的过程中,流程会有一些差别。
为了突出重点,本节将不再介绍整体流程,而是只介绍和跨机逻辑不同的地方。
有差异的地方总共有两个,分别是路由和驱动程序。
对于本机网络 IO 来说,特殊之处在于在 local 路由表中就能找到路由项,对应的设备都将使用 loopback 网卡,也就是我们常见的 lO。
从上述结果可以看出,对于目的是 127.0.0.1 的路由在 local 路由表中就能够找到了。
对于是本机的网络请求,设备将全部都使用 lo 虚拟网卡,接下来的网络层仍然和跨机网络 IO 一样。
本机网络 IO 需要进行 IP 分片吗?
因为和正常的网络层处理过程一样,如果 skb 大于 MTU 的话,仍然会进行分片。
只不过 lo 的 MTU 比 Ethernet 要大很多。
通过 ifconfig 命令就可以查到,普通网卡一般为 1500,而 lO 虚拟接口能有 65535。
为什么我把“驱动”加个引号呢,因为 loopback 是一个纯软件性质的虚拟接口,并没有真正意义上的驱动。
在邻居子系统函数中经过处理,进入到网络设备子系统,只有触发完软中断,发送过程就算是完成了。
在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。
而在本机的网络 IO 过程中,由于并不真的过网卡,所以网卡实际传输,硬中断就都省去了。直接从软中断开始,送进协议栈。
网络再往后依次是传输层,最后唤醒用户进程,这里就不多展开了。
我们来总结一下本机网络通信的内核执行流程:
回想下跨机网络 IO 的流程:
通过本文的叙述,我们确定地得出结论,不需要经过网卡。即使了把网卡拔了本机网络是否还可以正常使用的。
总的来说,本机网络 IO 和跨机 IO 比较起来,确实是节约了一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。
但是在内核其它组件上可是一点都没少:系统调用、协议栈(传输层、网络层等)、网络设备子系统、邻居子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东东)。所以即使是本机网络 IO,也别误以为没啥开销。
先说结论:我认为这两种使用方法在性能上没有啥差别。
我觉得有相当大一部分人都会认为访问本机server 的话,用 127.0.0.1 更快。原因是直觉上认为访问 IP 就会经过网卡。
其实内核知道本机上所有的 IP,只要发现目的地址是本机 IP 就可以全走 loopback 回环设备了。
本机其它 IP 和 127.0.0.1 一样,也是不用过物理网卡的,所以访问它们性能开销基本一样!
How SKBs work - Linux kernel
http://vger.kernel.org/~davem/skb.html
一篇解读Linux网络协议栈
https://zhuanlan.zhihu.com/p/475319464
你真的了解127.0.0.1和0.0.0.0的区别?
http://www.52im.net/thread-2928-1-1.html
深入 *** 作系统,彻底搞懂127.0.0.1本机网络通信
http://www.52im.net/thread-3590-1-1.html
/*************************************文件名: server.c
linux 下socket网络编程简例 - 服务端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
作者:kikilizhm#163.com (将#换为@)
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sfp,nfp /* 定义两个描述符 */
struct sockaddr_in s_add,c_add
int sin_size
unsigned short portnum=0x8888 /* 服务端使用端口 */
printf("Hello,welcome to my server !\r\n")
sfp = socket(AF_INET, SOCK_STREAM, 0)
if(-1 == sfp)
{
printf("socket fail ! \r\n")
return -1
}
printf("socket ok !\r\n")
/* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */
bzero(&s_add,sizeof(struct sockaddr_in))
s_add.sin_family=AF_INET
s_add.sin_addr.s_addr=htonl(INADDR_ANY) /* 这里地址使用全0,即所有 */
s_add.sin_port=htons(portnum)
/* 使用bind进行绑定端口 */
if(-1 == bind(sfp,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("bind fail !\r\n")
return -1
}
printf("bind ok !\r\n")
/* 开始监听相应的端口 */
if(-1 == listen(sfp,5))
{
printf("listen fail !\r\n")
return -1
}
printf("listen ok\r\n")
while(1)
{
sin_size = sizeof(struct sockaddr_in)
/* accept服务端使用函数,调用时即进入阻塞状态,等待用户进行连接,在没有客户端进行连接时,程序停止在此处,
不会看到后面的打印,当有客户端进行连接时,程序马上执行一次,然后再次循环到此处继续等待。
此处accept的第二个参数用于获取客户端的端口和地址信息。
*/
nfp = accept(sfp, (struct sockaddr *)(&c_add), &sin_size)
if(-1 == nfp)
{
printf("accept fail !\r\n")
return -1
}
printf("accept ok!\r\nServer start get connect from %#x : %#x\r\n",ntohl(c_add.sin_addr.s_addr),ntohs(c_add.sin_port))
/* 这里使用write向客户端发送信息,也可以尝试使用其他函数实现 */
if(-1 == write(nfp,"hello,welcome to my server \r\n",32))
{
printf("write fail!\r\n")
return -1
}
printf("write ok!\r\n")
close(nfp)
}
close(sfp)
return 0
} /*************************************
文件名: client.c
linux 下socket网络编程简例 - 客户端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
作者:kikilizhm#163.com (将#换为@)
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int cfd /* 文件描述符 */
int recbytes
int sin_size
char buffer[1024]={0} /* 接受缓冲区 */
struct sockaddr_in s_add,c_add /* 存储服务端和本端的ip、端口等信息结构体 */
unsigned short portnum=0x8888 /* 服务端使用的通信端口,可以更改,需和服务端相同 */
printf("Hello,welcome to client !\r\n")
/* 建立socket 使用因特网,TCP流传输 */
cfd = socket(AF_INET, SOCK_STREAM, 0)
if(-1 == cfd)
{
printf("socket fail ! \r\n")
return -1
}
printf("socket ok !\r\n")
/* 构造服务器端的ip和端口信息,具体结构体可以查资料 */
bzero(&s_add,sizeof(struct sockaddr_in))
s_add.sin_family=AF_INET
s_add.sin_addr.s_addr= inet_addr("192.168.1.104") /* ip转换为4字节整形,使用时需要根据服务端ip进行更改 */
s_add.sin_port=htons(portnum) /* 这里htons是将short型数据字节序由主机型转换为网络型,其实就是
将2字节数据的前后两个字节倒换,和对应的ntohs效果、实质相同,只不过名字不同。htonl和ntohl是
*** 作的4字节整形。将0x12345678变为0x78563412,名字不同,内容两两相同,一般情况下网络为大端,
PPC的cpu为大端,x86的cpu为小端,arm的可以配置大小端,需要保证接收时字节序正确。
*/
printf("s_addr = %#x ,port : %#x\r\n",s_add.sin_addr.s_addr,s_add.sin_port) /* 这里打印出的是小端
和我们平时看到的是相反的。 */
/* 客户端连接服务器,参数依次为socket文件描述符,地址信息,地址结构大小 */
if(-1 == connect(cfd,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("connect fail !\r\n")
return -1
}
printf("connect ok !\r\n")
/*连接成功,从服务端接收字符*/
if(-1 == (recbytes = read(cfd,buffer,1024)))
{
printf("read data fail !\r\n")
return -1
}
printf("read ok\r\nREC:\r\n")
buffer[recbytes]='\0'
printf("%s\r\n",buffer)
getchar() /* 此句为使程序暂停在此处,可以使用netstat查看当前的连接 */
close(cfd) /* 关闭连接,本次通信完成 */
return 0
}
方法/步骤1
1.服务器端的代码:
void
SocketServer::CreateConnect(){std::cout<<"start Create
Socket!"<<std::endl m_nSocket = -1 struct sockaddr_in
server_addr struct sockaddr_in client_addr int
portnumber=4321 if((m_nSocket=socket(AF_INET,SOCK_STREAM,0))==-1)
{std::cout<<"socket Create
Failed!"<<strerror(errno)<<std::endl return }
std::cout<<"Create Socket Finish!"<<std::endl/*
服务器端填充 sockaddr结构 */bzero(&server_addr,sizeof(struct
sockaddr_in)) server_addr.sin_family=AF_INET
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1")
server_addr.sin_port=htons(portnumber) /* 捆绑sockfd描述符 */
if(bind(m_nSocket,(struct sockaddr *)(&server_addr),sizeof(struct
sockaddr))==-1){std::cout<<"Server Bind
Failed!"<<strerror(errno)<<std::endl return }
std::cout<<"bind Socket Finish!"<<std::endl /*
监听sockfd描述符 */if(listen(m_nSocket,5)==-1){
std::cout<<"Listen
Failed!"<<strerror(errno)<<std::endl return }
std::cout<<"listen Socket Finish!"<<std::endl
while(1){/* 服务器阻塞,直到客户程序建立连接 */
std::cout<<"listen start..."<<std::endl int
nNewFd=-1 size_t nSize = sizeof(struct sockaddr_in)
if((nNewFd=accept(m_nSocket,(struct sockaddr
*)(&client_addr),&(nSize))) == -1){
std::cout<<"accept
Failed!"<<strerror(errno)<<std::endl return
}std::cout<<"server Get Connect
from:"<<inet_ntoa(client_addr.sin_addr)<<std::endl
while(1){char buf[256] = "0" size_t
len = recv( nNewFd, buf, 256, 0)if ( len >0)
std::cout<<"接受的消息为:"<<buf<<std::endl
else {if ( len <0)
std::cout<<"错误为:"<<strerror( errno)<<std::endl
else
std::cout<<"客户端断开:"<<std::endl break
}}/* 这个通讯已经结束 *///close(nNewFd)
/* 循环下一个 */}}
2
2.客户端代码:
//
创建链接void SocketClient::CreateConnect(){char buffer[1024] struct
sockaddr_in server_addr std::cout<<"socket create
start!"<<std::endl /* 客户程序开始建立 sockfd描述符 */
if((m_nSocket=socket(AF_INET,SOCK_STREAM,0))==-1){
std::cout<<"Client socket Create
Failed!!"<<strerror(errno)<<std::endl return }
std::cout<<"socket create finshed!"<<std::endl /*
客户程序填充服务端的资料 */int portnumber = 4321
bzero(&server_addr,sizeof(server_addr))
server_addr.sin_family=AF_INET
server_addr.sin_port=htons(portnumber)
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1") /*
客户程序发起连接请求 */if(connect(m_nSocket,(struct sockaddr
*)(&server_addr),sizeof(struct sockaddr))==-1){
std::cout<<"Client connect
Fialed!"<<strerror(errno)<<std::endl return }
std::cout<<"connect finshed!"<<std::endl std::string
str="start..." while(!str.empty()){
std::cout<<"starting....!"<<std::endl
std::cin>>str size_t len = send ( m_nSocket, str.c_str(),
str.length(), 0)if ( len <0){
std::cout<<"消息发送失败"<<str<<strerror
(errno)<<std::endl }else{
std::cout<<"消息发送成功"<<str<<std::endl }}
close(m_nSocket)}
3
3.编译完成后,启动服务器端代码和客户端代码就可以进行通信了。
4
4.
普及一下TCP/IP协议的部分知识,大家都知道TCP是面向链接的协议,即在进行通信前服务器是被动链接,客户端是主动链接,那么客户端与服务器端的连
接需要经过三次握手才能建立链接。在整个通信过程中客户端与服务器端的进行的通信都有确认机制,保证包能顺利的到达对方,但这也不是100%,中间的路由
等中间桥出现问题,也有可能造成数据包的丢失,而任何一方没有收到确认包,都会一直处于等待和重传的过程中...
服务器:
创建socket->bind->listen->recv(send)->close
客户端:
创建socket->connect->recv(send)->close
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)