深入浅出谈谈网络协议栈的实现原理

1. 网络传输过程中的层次分布如下,tcp/ip协议中,应用层协议,如http,https,websocket协议,属于用户程序自定义的协议,而传输层(tcp/udp),网络层,数据链路层都是Kernel程序帮助实现。

 2.在socket编程里面,服务器端一共有以下这些系统调用API,socket(),  bind(),listen(),accept(),recieve(),send(),close(); 头文件:

#include <sys/types.h> //listen()
#include <sys/socket.h> //socket()
#include <arpa/inet.h>  //hton ntoh
#include <netinet/in.h> //inet_addr

#include <errno.h>     //获得系统调用出错的原因
#include <unistd.h>    //可以使用read(),write()
#include <string.h>   //strerro()捕获出错原因

先要创建一个socket,实际上内核创建了一个文件,可以通过该文件来进行网络内容的加载,这也是为什么我们可以使用read()函数读出来内容,write()函数写进网络内核协议栈的原因。

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
//该套接字支持ipv4,sock_stream(tcp)协议,0是默认参数,与其他API适配

在Linux系统中,stdin的文件为0,stdout为1,stderror为2,如果系统没有其他的fd处理,返回的fd将为3,在创建一个socket,创建成功,则fd=4;  当返回的fd为负数的时候,说明创建失败,可以使用strerror(errorno)读出来错误的原因;这个fd或者称为listenfd作为进行网络连接的窗口,如有客户端通过tcp连接进来,该fd将完成与客户端的三次握手。

  struct sockaddr_in addr;  //创建一个sockaddr_in结构体,将网络ip,port指定起来
  memset(&addr, 0, sizeof(struct sockaddr_in)); 
  addr.sin_family=AF_INET; //使用ipv4协议
  addr.sin_port=htons(port); //端口号,转为网络字节序
  addr.sin_addr.s_addr=INADDR_ANY; //ip绑定本机全部的网卡地址,也可以绑定具体的ip,需转为网络字 
                                    //节序,调用htonl();

  int ret=bind(fd,(struct sockaddr*)&addr,sizeof(sockaddr_in)); 
  if(ret<0)  printf("bind error %s\n", strerror(errno));
  
  ret= listen(fd,20);
  if(ret<0)  printf("listen error %s\n", strerror(errno));
  	
  struct sockaddr_in addr;
  len=sizeof(struct sockaddr_in);
  memset(&addr,0,len);

  int clientfd = accept(fd, (struct sockaddr*)&addr, &len);
  if (clientfd <= 0)  printf("accept failed\n");

  char* buf[1024];
  
  size_t nums= send(clientfd, buf, 1024, 0); 
  size_t re= recv(clientfd, buf, nums, 0);
  
  if(close(clientfd)>0) close(listenfd);




    

逐步讲解上面这段代码:

 bind()函数,将ip,port,listenfd,传输层协议绑定起来,使用这五元组,就标识了一个网络通信的进程;如果bind()失败,则返回负数。

listen()函数,是使用tcp传输时独有的,将监听与本地连接的客户端程序,先来看看tcp三次握手过程:

syn队列也叫半连接队列,里面储存第一次发syn包的客户端;Accept队列也叫做全连接队列,里面储存的是完成3次握手之后的客户端。listen(fd,backlog),fd是最开始建立的listenfd,backlog就是a全连接队列的长度,当建立连接以后,内核将把该客户端的建立信息从该队列中删除。

accept()函数,accept(fd,(sockaddr*)&addr,&len),其中的addr是一个带入带出参数,将获取来连接的客户端的ip,port信息;返回值是clientfd,也即是用来建立信息发送的fd,如果返回-1,则说明accept失败;另外,在非设置下,该函数是阻塞函数,如果没有客户端连接,则程序将阻塞在这里,可以通过以下代码进行设置:

int flags = fcntl(clientfd, F_GETFL, 0); //获取clientfd的标志位
fcntl(clientfd, F_SETFL, flags | O_NONBLOCK); //位或操作,将标志位设置为非阻塞

可以看到非阻塞是对fd进行操作,也即是对文件描述符进行操作,那么,该fd下的send,recieve,read,write函数都将是非阻塞的函数;如果内核协议栈没有数据可读,将立即返回,而不阻塞程序。

send()函数,原型: send(int sockfd, const void *buf, size_t len, int flags);带有4个参数,发送的fd,发送内容,发送的长度,最后一个int flags为0,内核设计者为了与其他API适配,加了最后这个参数,所以如果封装send(),只需要三个参数,send将内容发送到内核缓冲区;函数返回值为实际发送的字符串的长度;

recv()函数:与send()参数一样, recv(int sockfd, void *buf, size_t len, int flags);接收的fd,接受内容,接受长度,最后一位为0,原理与send一样;将内容从内核协议栈读到用户空间中来,返回值为实际读到的字符长度。

close()函数,调用该函数表明关闭一个文件,该函数并不是POSIX API,只是linux系统调用;函数返回值为-1,则说明文件关闭失败;当关闭文件clientfd时,就会发生tcp四次挥手:

调用close方发送finish包,表明想要关闭该连接,j进入fin_wait1阶段,另一方发送ack确认包,这时候主动方进入fin_wait2阶段,确认自己的网络信息处理是否完毕,完成后,被动方发送finish包,确认无网络数据处理, 主动方将发送ack确认包,说明网络数据处理完毕。

最后主动发还是要time_wait一段时间,原因是因为:

(1. 在网络中,报文可能会因为网络延迟、丢失或重传而导致传输时间不确定。通过等待一段时间,可以确保对方收到了自己的 ACK 报文。

(2. 在 `TIME_WAIT` 状态下,客户端不会接受来自相同连接的新报文段。这是为了防止旧的报文段在网络中滞留,并被错误地传递到新的连接中。

(3. 在 `TIME_WAIT` 状态下,旧连接的资源(如套接字、缓冲区等)可以被完全释放。这样可以确保在关闭连接后,不会有任何未完成的数据或资源残留。

                       ---------------------------------------------------------------------------------------------------------end

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值