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