轻量级tcp框架
By boywhp@126.com

  一.系统数据层次
  1、Sock接口层
     实现Sock接口以及网络数据包的组装、投递。对应用层接口如下:
struct tcp_link_info{
pvoid on_send_done;//tcp发送确认完成

}

struct sock_buf{
sock_buf* next;            //下一个数据包
pvoid head_protos[MAXS_PROTS];  //协议层信息,指向自身buf位置
char *head;                //封装协议头时候,head向下递减
Int status;
int size;
char buf[MAX_SOCK_BUF];
}
head_protos安装具体协议层次:
ARP,IP,[ICMP,TCP,UDP]
收、发送预先设置好默认指针!


Int sock_create(int style, ulong bind_ip);
创建一个sock并绑定于指定ip地址
Int tcp_connect(int sock, ulong ip, word port);
尝试建立tcp连接
Int tcp_get_info(struct *tcp_link_info);
Int tcp_put_info(struct *tcp_link_info);
设置以及获取tcp传输信息
Int tcp_send(int sock, pvoid buf, int size);
发送tcp数据,不确保发送成功(写入发送队列)。
Int tcp_recv(int sock, pvoid buf, int size,int wait);
接收tcp数据<-buf,wait指明等待模式
Int sock_close(int sock);
关闭sock

Socket{
pvoid proto;       //最上层的协议 tcp,udp,arp,raw
int status;       //closed,ready,XXX
sock_buf* send_skb;  //在该socket上的发送队列
Sock_buf* recv_skb;  //在该socket上的接收队列
}
sock是一个句柄,内部通过一个struct socket[MAX_SOCK_NUM]的表管理socket, socket[sock]即可访问socket!

Int insert_sock_send(socket, skb)
插入一个skb数据包到socket发送队列
Int insert_sock_recv(socket, skb)
插入一个skb数据包到socket接收队列

  2.协议传输层
Struct proto_lay{
Proto_lay * prev;//协议双链表
Proto_lay * next;
Proto_lay * next_hash;//处理hash值重复问题
sock_buf* send_skb;      //待发送到skb链
Socket* sock;
Int send_skb(context, skb);
Int on_send_skb_done(context, skb);
Int get_mcu();
Int on_recv_skb(context, skb);
int type;
pvoid alloc_context();//申请上下文
pvoid free_context(pvoid conext);//释放上下文
Pvoid conext;  //根据type决定conext的具体类型
}

Alloc_proto_lay(int type);
Free_proto_lay();

Struct proto_tcp
{
Struct proto_lay;  //继承协议层接口
Int tcp_status;
Int tcp_windows;
Int tcp_seq;
Int tcp_ack;
...
};

proto_tcp* New_proto_tcp();
proto_tcp* Release_proto_tcp();
Void Init_proto_tcp(proto_tcp* tcp);
Int send_tcp_skb(proto_tcp* tcp, skb);

Struct proto_ip{
Struct proto_lay;
Ulong dst_IP;
Ulong src_IP;
....
}

  3.网卡接口
struct net_filter{
Proto_lay* proto;
Int check_pack();
}
//??如何快速的识别数据包,并分发到对应的proto_lay?
通过hash表:
       Eth层   IP层           TCP/UDP层
Hash = style1 + [style2 + IP]  +[style3 + PORT]
计算出Hash值后:
proto_lays[PROTO_MAX_HASH_NUM]; //动态分配
直接递交给proto_lays[hash]->on_recv_skb
如何处理Hash重复的问题!!!添加一个proto_lay->next_hash,形成单链表


struct net_adapter{
Int net_send(pvoid head, pvoid buf, int size);
Int on_net_recv(pvoid frame, int size);
Filter* filter_list;  
}
Reg_net_adapter(net_adapter* adapter);

  4.lookaside内存管理
lookaside内存管理(管理大量相同数据大小的内存申请和释放)
Struct look_aside_pack{
Look_aside_pack* next;
Int pack_max;
Int pack_size;
PBIT mask;         //数据区masks[pack_max]
Pvoid pack;       //实际数据区packs[pack_max]
}
Struct look_aside_pools{

Look_aside_pack* pack[HASH_SIZE];  //256 hashs - 长度 & FF
}

Alloc_look_aside_pack();



  5、socket输入输出环形缓冲区
Struct ring_buf{
Int size;
Pvoid out_buf;
Pvoid in_buf;
Pvoid buf;
}

ring_buf* alloc_ring_buf(size); //申请环形缓存
Void release_ring_buf(size);//释放环形缓冲
Int write_ring_buf(ring_buf, buf, size);//写入环形缓存
Int read_ring_buf(ring_buf, buf, size);
读取环形缓存,buf=NULL,不拷贝,直接移动!!以便实现0拷贝


???全局proto_lays[MAX_PROTO_LAYS_NUM];通过proto_id即可获取协议层
具体的传输细节处理?怎么实现?如tcp控制窗口以及重传等机制如何实现?

  6、sock异步接口
Struct sock_tcp_events{
  On_connect(int sock);     //连接成功
  On_request(int sock);     //连接请求 - 可以暂不实现,自动建立连接
  On_send_completed(int sock, int size); //发送成功-ACK确认
  On_recv(int sock, buf, size);//接受到数据包
}
如何实现一个端口上listen<-接受多个tcp连接?在listen状态下的80端口,并没有目的端口,如何接受数据?即并没有hash值!!
每个最上层的协议提供一个自己的hash计算函数:
Int make_tcp_recv_hash(struct skb);
XXXX
listen时候 hash = tcp协议 + listen端口
其他时候:hash = tcp协议 + src_port + dst_port + src_ip + dst_ip



  二.数据基本流程
  1.数据发送
1、  tcp_send(int sock, buf, size)
2、  通过sock号获取Socket数据结构
3、  写入socket的环形输入缓冲区
4、  主线程循环读取环形缓冲区,测试是否有数据发送。
5、  申请lookaside skb数据结构,初始化并插入Socket发送队列
4、获取socket->proto,[proto_lay]数据结构
5、调用proto_lay->send_skb发送数据包
6、调用proto_tcp->tcp_send_skb封装tcp头
7、调用proto_tcp->next->send_skb,发送到下层协议
8、调用proto_ip->send_skb封装ip头

  2.数据接收
1、adapter->on_net_recv(pvoid frame, int size);
2、从下到上分析proto层次,计算hash值
3、通过hash表获取接收协议的proto_lay层次
4、组skb包
5、遍历next_hash链表,依次调用proto_lay->on_recv_skb(skb);
6、按协议双链表向上依次执行proto_lay->on_recv_skb(skb);解协议包
7、一旦proto_lay->sock!=NULL,将skb挂入socket->recv_skb队列
8、用户通过tcp_recv(sock);进行数据接收

  3.TCP连接的建立
1、tcp_connect(sock,ip,port)
2、(tcp_proto*)(socket->proto)->connect(tcp_proto*, ip,port)
3、根据tcp协议状态图执行,记录dest_addr,dst_port,src_port
4、new_skb(),insert_sock_send(sock, skb) //sock写入发送队列
5、tcp_proto->send_skb(skb),next->dst_Ip = dest_addr.ip;
测试skb->status,写入tcp syn连接数据.
6、next->send_skb(skb)
主动连接:
1  tcp_proto->connect = tcp_proto_connect(主动连接)
2  初始化skb填写tcp连接头
3  tcp_send_skb发送该skb连接包,将skb挂入发送队列
4  等待on_tcp_recv接受服务段syn+ack 或者超时重发syn连接包(超时重发:直接将包->ip层)
5  on_tcp_syn_send_recv接受到syn+ack,申请skb应答ack并发送
6  完成3次握手->进入数据状态

  4.数据包的拆分以及重组
合并:大量的待发送的小数据包,将在发送队列进行重组。首先检查发送队列是否为空,不空的话就尝试和下一个数据包合并,即tcp粘包现象。可以强制立即发送,不等待发送线程调度。
拆分:如果大数据包进入队列,进行数据拆分,依次进入发送队列。或者发送当前队列数据包时候超过发送协议MCU,自动拆分数据包
队列操作涉及到线程锁问题!!!

Socket输入输出缓存不使用skb队列,直接读写环形缓冲区。!!!

  5.数据包0拷贝的实现
数据包在未确认发送成功时,不移动环形缓冲区的读位置,其数据并不拷贝到skb中,只是记录在skb->head_protos[]中,skb->buf中只记录数据协议头封包。最终发送给网卡net_send(head, buf, head_size, buf_size)实现发送0拷贝。
  6.数据发送线程
  7.tcp超时重传问题
注意数据包的重传机制位于协议层,每个协议层维护一个skb链表。以便实现超时重传。由于tcp滑动窗口协议,不会导致skb重组,因此不必变动skb包,只需超时重发即可。
一旦接受到确认ack,tcp计算确认的长度,然后遍历skb发送链路。删除已确认的skb!不允许出现确认skb包的一部分!

  8.tcp一个端口监听,多个tcp连接
tcp在指定端口监听后,如何应对多个tcp连接请求,并建立tcp通信链路:提供accept调用。返回建立连接的socket链路!tcp在src_port上监听, accept调用在线程阻塞等待tcp连接建立,一旦连接建立,创建一个tcp_socket,初始tcp连接参数,设置接受hash = src_port+dst_port,返回创建的socket,accept同时提供异步接口,由用户查询返回结果,或者通过注册tcp_events回调函数实现。流程如下:
1.on_tcp_listen_recv 远程主机SYN连接请求,应答SYN+ACK
2.等待ACK,建立三次握手,sock_tcp_events->on_request
3.on_tcp_request(sock.c),测试socket.accept_max参数
4.创建一个tcp_socket,设置socket参数 
5.初始化tcp连接,注册接受函数
6.将socket挂入tcp->accept_header队列
7.用户程序调用sock_accept
8.从socket->tcp.accept列表里面获取等待accept的socket,并从链表中删除

  9.tcp分组数据接收以及重排
一旦接收到的tcp_hdr->seq > tcp.ack时候,说明需要接收tcp数据,此时tcp处理流程如下:
1.申请一个skb, skb的数据指针->对应socket的环形缓冲区recv_buf
2.将接收的数据拷贝到recv_buf中,位置通过seq计算【相对in_buf指针位置】。
3.检查当前接收数据包序号是否可以立即接收,不需要重排。
4.将需要重排的数据包按照seq次序插入接收队列,同时发送ack-以通知发送可立即接收的数据包。
5.如果是可以立即接收,则检查接收队列,序号连续,并in_ring_buf,发送最后一个确认ack!
  10、tcp接收0 windows问题
在同步接收数据模式下,由于发送方数据填充windows,接收数据部分处理不及时,出现应答ack0窗口现象,导致tcp发送方停止5s左右再发送数据。为避免该现象,应该延迟发送ack,只有在接收方有足够空闲时候发送ack,且不能阻塞tcp主接收线程。方案如下:
1.insert_skb_recv_buf 将接收到的skb,复制到缓冲区并按照seq大小插入recv_skb_header链表。
2.sock_read从recv_buf中同步out数据
3.main_thread[定时器]中,检查sock接收缓冲,如果空闲空间足够大,调用sock->recv_buf_free。
4.tcp_free_recv_skb释放skb接收队列->tcp.ack并回应ack


  三.实现步骤
  1.完成通用内存管理、hash-obj表
hash-obj表:主要用来动态申请obj,以及通过hash的快速查找obj,结合内存管理实现obj管理。
Struct hash_obj{
int hash;
int obj_size;
pvoid object;
}

Struct hash_obj_pool{


int max_obj;
hash_obj* hash_table;
}

Int init_hash_obj_pool(hash_obj_pool*, max_objs, size);
viod free_hash_obj_pool(hash_obj_pool*);
hash_obj* alloc_hash_pool_obj(hash_obj_pool*,obj_size);
viod release_hash_pool_obj(hash_obj_pool*, pviod obj);
pvoid* get_hash_pool_obj(hash_obj_pool*, int hash);
从hash表中快速获取obj, hash_table[hash]->object

编写内存位操作支持函数,以便内存管理mask
void set_bit(void *addr, int pos);
void clr_bit(void *addr, int pos);
int get_first_bit(void *addr, int size);

Init_hash_table(hash_max);
Insert_hash_table(int hash, pvoid obj);
Pvoid Get_hash_table(int hash, pvoid obj);


  2.sock接口层sock_create
socket定义,socket-hash表,完成hash表测试,
Sock_create流程:
1、通过hash_obj管理申请一个socket
2、初始化socket,输入输出环形缓冲区、状态以及最上层proto_lay;
专门一个tcp_lay_pool,[tcp_lay];tcp_lay.proto.context = tcp_lay;
alloc_tcp_lay();
init_tcp_lay();   //设置函数指针以及协议链
alloc_ip_lay();
init_ip_lay();
Socket->proto = tcp_lay;
  3.环形缓冲区
测试OK!struct sock_ring_buf

  4.网络数据收发接口(测试环境)
Ok~struct sock_net_nic
  5.网络数据发送线程
1、发送线程扫描每个socket的发送环形缓冲队列
2、获取协议层的当前最大包长度MSS,以及允许发送长度Windows
3、发送线程申请skb数据包,填充Windows,包大小为MSS,如果缓冲区回环,则切割为两个skb包,以便0拷贝技术【不读取实际数据和移动环形缓冲区读写指针!/只有在确认发送成功后移动读指针】。

上传的附件 tinyshell.rar