第十八章 SOCKET类的实现

时间:2022-12-15 11:05:44

                        第十八章    SOCKET类的实现

 

      这几天反复思考,到底是从上到下、还是从底层开始往上设计?最后、还是决定从上层建筑开始。APO追求的是简单、再简单!强大、再强大!高速、高效!“天下武功、无坚不破、唯快不破!”。

      APO的socket也不外是一种内存文件吧,但socket描述符和其它类型的文件描述符还是略有区别的。APO中的一个用户进程最多可打开64K个非socket类型的文件描述符,而APO系统只是最多可以打开16M个v节点,需要256个64K位的位图变量、刚好1个位图变量数据块;中模式管理。socket描述符也是在16M个文件描述符中分配,APO中的socket描述符、描述的是一个连接、可以说是连接号,与UNIX可能会有一点不同;理论上、APO可以在一个服务端口上最大支持1千6百多万个连接。APO的网络编程是非常简单的,如服务端程序、只是在初始化方法中open一个socket,余下的只是对各种连接消息的处理方法的编写。即使有1千万个连接,那又能怎样?对于它们的请求消息的处理方法都是一样的。用户无须关心打开了多少socket描述符,有多少连接,关闭连接,TCP/UDP、管理socket描述符等等,那都是用户系统网络进程的事情。对于用户的网络编程,我们就是要越简单越好。服务端程序只是关心、来了一个带请求的连接消息,需要处理该请求消息,使用消息中的socket描述符就可操作该连接对应的接收、或发送流容器;至于、该连接是如何建立起来的、当前是否有1千万个连接等等,不用管的。

     LINUX、WINSOCKET中的bind()、socket()、listen()、accept()、connect()、write()、read()、recvfrom()、sendto()、upds_respon()、recv()、send()、recvmsg()、sendmsg()、close、shutdoen()、循环服务器、并发服务器、多路I/O复用、原始套接字等等、我都看晕了,这难道是给应用程序员使用的网络编程方法库?我只能感叹!真垃圾!这是底层程序员使用的、不是应用程序员啊。发展了几十年,应该是精华的浓缩了;为何我总看不顺眼?总感觉这样做、复杂、效率低,为何聪明人总是把问题复杂化啊?我也很想兼容和适应它们,但总是有心无力;自己笨、没法把握他们的思路。不管怎样,服务端支持千万个连接的功能我是加定了;这样,1000个游戏服务器就可支持100亿人同时在线了。

     在APO中,网络编程是分为三层:第一层SOCKET应用层,第二层是TCP/UDP(用户CPU线路的系统网络服务进程)、第三层是最底层IP/ICMP(内核CPU线路的实时线程)。层之间是使用消息交互、句柄提交、事件驱动。后2层是系统实现,用户主要是使用第一层编程。SOCKET是一种内存文件,网络编程分为服务端和客户端,网络协议主要就是TCP/UDP;在open文件SOCKET时、就需指明。open、close本来就是文件系统的基本方法,本文只是描述与socket有关的内容。数据报接收是2、3层自动实现的,包含数据报内容的句柄就是相应socket连接的文件号sockfd,以消息方式提交给用户进程;用户只需使用sockfd.成员的赋值指令,就可对接收的数据报进行读、写。不同的文件号sockfd句柄、对应不同的连接,有不同的内存容器。只要本地内存空间够大,支持千万个连接就没问题。内存分配是无须用户去考虑,只要给出规划就行了。为何?当我们编写一个客户端程序时,无法预先知道你请求之后、服务器返回的数据报大小,可能2GB;也就无法一开始就声明接收流容器的本地内存空间大小。但接收到服务器返回的数据报后,底层知道;所以、就可由底层申请分配内存,并将句柄关联到相应的sockfd。那接收就似乎无须用户规划了,但你发送的请求数据报是可以预设大小的。如果客户端,请求一个4GB大小的文件,难道服务器就要即时申请一个连续4GB大小的内存空间?用户编写的服务端程序无须这样麻烦,只是向2层发一个消息就行了(sendto())、一切2层搞掂。或许APO在SOCKET层就3个方法了:open()、close()、sendto()。前2个本来就是文件系统的基本方法,open()方法的参数有所不一样吧;所以改为opens()。用户程序主要是编写消息处理;实现HTTP协议、FTP等等。其实、即使这些协议的实现;APO操作系统都包含进去了。用户主要就是编写游戏等服务端的消息处理吧。

    当是客户端时,TCP/UDP的端口可以对应到sockfd、进程号、或线程号。但如是服务端,就不一样了;虽然能对应到进程号,但可能有几百万个连接、sockfd文件号。所以,服务端应设置流标签(低24位) = sockfd文件号(连接号);这样、下次客户端的带有相同流标签(连接号)的请求到来时,服务端的底层就可迅速定位到相应的连接v节点(当然还需要比对一下)。

一、服务端程序

    APO的进程就是一个消息恒循环,用户代码入口main(); 就是执行用户编写的进程消息处理代码。我们只需要在进程共用部分的初始化方法中Process_init(); 加入以服务端方式、TCP或UDP、最大连接数BACKLOG(单位为256个连接)、客户端发来的请求报文最大长度RECVLEN(单位行E)、服务端的发送流容器变量、的opens()方法。那么,余下的就是第二层TCP/UDP、用户CPU线路的系统网络服务进程的事情了。每当有一个新的属于服务进程的TCP/UDP数据报到来,第二层会给服务进程的动态优先级加码、以便服务进程可以更快的得到CPU;同时给服务进程发出一个消息(包含连接号sockfd、状态码等等)。当服务进程获得CPU时,可能已经有n个消息了;1、系统读入一个消息到H2行寄存器,服务进程处理该消息;2、判断、如果是第二层的系统网络服务进程发出,那么是网络数据报消息,用sockfd.成员,提取数据报、分析是什么请求、处理该请求,最后回发一个服务器数据报。3、循环回到1,如果没有需处理的消息、服务进程让步挂起返回。至于是如何构建一个连接,有上千万个连接、数据报的内存分配、IP包组装等等;都不是应用层需关心的,那是底层的事情。所以,APO的服务端程序没料啊,就一个opens()和回发服务器数据报时、可能使用到的sendto()(该方法与LINUX、UINX、WINDOWS的有区别)、或关闭一个连接close()。那些、bind()、socket()、listen()、accept()、connect()、write()、read()、recvfrom()、upds_respon()、recv()、send()、recvmsg()、sendmsg()、shutdoen()、sigio、select()等等、通通清除;还应用程序员一个干净的世界。要注意的是:如果同一个连接的客户端发了一个数据报,在服务进程还没处理完毕时,又发来一个数据报;客户端将受到流量抑制。如果前面的数据报达到1GB以上,客户端将立刻受到流量抑制、不允许再发数据报;直到服务进程处理完上一个数据报。至于、黑客回溯追踪,恶意TCP/UDP包判断,连接处理,重发等定时器,IP黑名单,攻击报警、包过滤等等都是底层的事情,用户不必关注。我知道UDP、可能上千万个连接问题不大,但TCP就需另外对待了;就那几种定时器就不好处理,难道要设计几千万个定时器?这不可能;有待研究。


二、客户端编程

    用户可以在进程、或线程内设置客户端TCP/UDP程序。还是那3个方法,但opens方法的参数设置不一样;也不是在初始化方法出现;在线程、或进程打开一个连接opens,那通常是需要用户关闭连接close。

   BUxE  msg; // 在主程序、声明报文发送流容器(x行)。

Thread_client(){ // 在线程内的客户端方法、线程入口。

  sockfd =opens( FLAG.PORT );

// 获得socket文件号, PORT = XX指定的客户端口,0由系统指定端口;

// FLAG = 0xA000(TCP)、0x8000(UDP);由第二层建立连接。

// 设置需连接的服务器端IP地址、端口等

  sockfd.vnode.MDA = ?; // 服务端的以太网目标地址

  sockfd.vnode.DLADD= ?;// 服务端的目标链路地址,DLADD + MDA目标IP地址

  sockfd.vnode.DPORT= ?; // 服务端的目的端口

  msg = ?; // 填写第一个消息报文。

  sendto( sockfd,flags, msg, len ); // 发送第一个数据报。

  PRET // 分支定点等待消息、阻塞返回;新的线程入口在下一条指令。

  msgpro(); // 服务器返回的数据报处理方法;消息在H2,线程新入口。

// 收到一个服务器数据报、由二层返回一个消息到相应进程,而进程将启动相应的

// 线程msgpro()方法来处理。

  If FLAG.关闭标志 = 1 then goto tclt; 

  msg = ?; // 填写消息报文。

  sendto( sockfd,flags, msg, len ); // 由第二层发送一个新的请求数据报。

  WRET  // 等待消息、阻塞返回,准备处理下一个数据报消息。

tclt:

  close(sockfd); // 关闭连接

  TRET  // 线程终止返回

 // msg也可以是关联到进程、或线程打开的一个普通文件号的流容器;这样,客户端也就可以上传一个文件(文件大小可到4GB);APO的数据报可以大到4GB。

三、opens()、sendto()、close()方法

   opens()方法只是把应用层的参数传入系统层(第二层),并返回一个文件号;客户端与服务端的参数是不一样的。对于返回的文件号sockfd,客户端才使用;而服务端是使用消息中的由第二层给出的新的连接文件号。

R0参数FLAG.PORT:高16位R0H标志FLAG、低16位R0L TCP/UDP的指定端口PORT。
APO的TCP服务端、oxE000,TCP客户端、oxA000;
APO的UDP服务端、oxC000,UDP客户端、ox8000。
// FLAG.15   DOMAIN;  协议域:1、INET_TYPE,0、其它。
// FLAG.14   NETMODEL; 网络模式:1、serve服务端模式,0、client客户端模式。
// FLAG.13   PROTOCOL; 协议:1、TCP(SOCK_STREAM),0、UDP(SOCK_DGRAM)。
// FLAG.12-8 STADE; 5位连接状态标志。
// FLAG.7-0  TYPE; 协议类型:
// PF_INET、IPAPO internet协议,PF_INET、IPV4 internet协议,
// PF_INET6、IPV6 internet协议,PF_IPX、NOVELL协议,
// PF_NETLINK、内核用户界面协议,PF_X25、X.25/ISO-8208协议,
// PF_AX25、AMATEUR RADIO AX2.5协议,PF_ATMPVC、原始ATMPVC访问,
// PF_APPLETALK、APPLETALK协议,PF_PACKET、底层包访问协议,

   socket文件是在内存建立的、和其它类型的文件不一样,无须i节点、目录项,也不需要文件打开表项;只有不一样的v节点,描述了一个连接。4E大小的v节点,前面2E是发送IP数据包的包头(包括MAC头、IP头、TCP/UDP头);后2E是连接描述,TCP/UDP的连接参数;下一章会详细说明。

1、服务端模式:sockfd = opens(FLAG.PORT, BACKLOG,RECVLEN );

BU16  BACKLOG; // 最大连接数、单位为256个连接。R1L

BU32  RECVLEN; // 低24位,允许的最大报文长度、单位E。R2


2、客户端模式:sockfd = opens( FLAG.PORT );

   需要使用返回的文件号来设置v节点中服务端的IP地址、端口号。

sockfd.vnode.MDA = ?; // 服务端的以太网目标地址

sockfd.vnode.DLADD= ?;// 服务端的目标链路地址,DLADD + MDA 就是目标IP地址

sockfd.vnode.DPORT= ?; // 服务端的目的端口

3、sendto()方法:数据报的接收是自动的,进程或线程只要根据第二层传来的消息做处理就行了;那么,要发送数据报就需使用sendto()方法了。APO中,不管TCP(强连接)、还是UDP(弱连接)都是通过连接来发送数据的,代表连接的是文件号sockfd、描述连接的是相应的v节点。客户端、服务端都有代表连接的文件号sockfd,与LINXU不一样的是:APO中的send()和sendto()没区别;也不需要复杂、垃圾的5种I/O模式。

sendto( sockfd, flags, msg, len ); // to是表示到第二层,并由底层(2、3层)发送数据报,flags一些发送数据报时的指示标志,msg是一个发送流容器变量(你想发送的信息数据报的地址引用和流容器大小);len是报文的实际长度(小于流容器大小)。


4、close(sockfd)方法:关闭一个连接,释放文件号、同时也向对方发送一个释放连接的数据报。