原创

Skynet服务器框架(六) Socket服务源码剖析和应用

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://linsh-tech.blog.csdn.net/article/details/70495328

引言:

如何在skynet框架中使用socket+protobuf。上篇 Skynet服务器框架(五) 使用pbc(protobuf)
我们已经大致了解了如何在Skynet中通过pcb来使用Protobuf,接下来我们开始了解skynet中有关Socket的部分。

socket_server C源码解析:

较早版本的skynet并没有提供对于网络层的定制,而是可以由开发者自行定义,不过最新版本的skynet已经加入了网络层的支持,也有独立的项目例子socket-server是纯C语言的实现。核心源码包括:socket_epoll.hsocket_kqueue.hsocket_poll.hsocket_server.csocket_server.h

1.异步IO

此网络库已经封装了socket的 epollkququ 两种底层接口,其功能就是:处理阻塞/非阻塞socket中 read/write 的问题,它们的区别在于适用于不同 操作系统 的通信接口的封装:

  • epoll (Linux2.6下性能最好的多路I/O就绪通知方法) -> Linux系统 (详见:【Linux学习】epoll详解
  • kqueue -> 其他Unix的变种系统(例如:FreeBSD)

假如是在windows下,可以使用 iocp模型 来实现类似的功能。

这一点可以从 socket_poll.h 的源码的宏定义中看出来:

//平台判断
#ifdef __linux__
#include "socket_epoll.h"
#endif

#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined (__NetBSD__)
#include "socket_kqueue.h"
#endif

socket_epoll.csocket_kqueue.c 中都实现了socket_poll.h中定义的方法,通过这个宏判断,假如当前运行于Linux系统下,就调用epoll的实现,而在FreeBSD系统中则使用kqueue的实现。

定义部分:

//统一使用的句柄类型
typedef int poll_fd;
//转存的内核通知的结构体
struct event {
    void * s;       //通知的句柄
    bool read;      //是否可读
    bool write;     //是否可写
};

核心接口:
这是poll用来管理socket事件或者消息的核心调用接口:

/**
*定义了外部使用的接口,具体实现在 socket_epoll.h 和 socket_kqueue.h 中定义
*/
//错误检测接口(fd: 检测的文件描述符(句柄),返回true表示有错误)
static bool sp_invalid(poll_fd fd);
//创建句柄(可通过sp_invalid检测是否创建失败,poll_fd是创建好的句柄)
static poll_fd sp_create();
//释放句柄
static void sp_release(poll_fd fd);
/*
 * 在轮序句柄fd中添加一个指定sock文件描述符,用来检测该socket
 * fd    : sp_create() 返回的句柄
 * sock  : 待处理的文件描述符, 一般为socket()返回结果
 * ud    : 自己使用的指针地址特殊处理
 *       : 返回0表示添加成功, -1表示失败
 */
static int sp_add(poll_fd fd, int sock, void *ud);
/*
 * 在轮询句柄fd中删除注册过的sock描述符
 * fd    : sp_create()创建的句柄
 * sock  : socket()创建的句柄
 */
static void sp_del(poll_fd fd, int sock);
/*
 * 在轮序句柄fd中修改sock注册类型
 * fd    : 轮询句柄
 * sock  : 待处理的句柄
 * ud    : 用户自定义数据地址
 * enable: true表示开启写, false表示还是监听读
 */
static void sp_write(poll_fd, int sock, void *ud, bool enable);
/*
 * 轮询句柄,等待有结果的时候构造当前用户层结构struct event结构描述中
 * fd    : sp_create()创建的句柄
 * e     : 一段struct event内存的首地址
 * max   : e内存能够使用的最大值
 *       : 返回等待到的变动数, 相对于e
 */
static int sp_wait(poll_fd, struct event *e, int max);
/*
 * 为套接字描述符设置为非阻塞的
 * sock  : 文件描述符
 */
static void sp_nonblocking(int sock);

接口具体实现在 socket_epoll.hsocket_kqueue.h 中定义

更详细的实现区别可以参考:《socket编程之select、poll、kqueue、epoll

2.socket-server测试实例:

下载纯C实现的socket-server源码后,先打开源码中给出的Test.c,并找到main入口:

int main() {
    //忽略对于SIGPIPE信号的默认处理
    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &sa, 0);
    //创建一个Socket服务实例
    struct socket_server * ss = socket_server_create();
    //测试方法
    test(ss);
    //释放socket实例相关的资源
    socket_server_release(ss);
    return 0;
}

上面操作就是使用socket_server最简单的例子,通过socket_server来控制和管理真正的Socket网络通信过程,实现步骤大致如下:

  • 创建一个socket_server实例;
  • 通过socket_server管理Socket通信;
  • 释放socket_server实例结束程序。

3.源码剖析:

  • 在程序入门位置设置忽略 SIGPIPE 信号:

    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &sa, 0);

    在网上检索资料《MonitorServer代码阅读笔记一:使用sigaction SIGPIPE, &sa, 0避免写一个已关闭的socket导致进程退出》了解到原因大致如下:

    • 在linux下写socket的程序的时候,如果尝试send到一个 disconnected socket 会让底层抛出一个 sigpipe 信号;
    • 对一个对端已经关闭的socket调用两次write, 第二次将会生成 sigpipe 信号。

    对于这个信号的默认处理方法是 退出进程 ,但是通常我们不希望按照这样来处理,所以通过上述代码屏蔽默认初始方法。

  • 创建Socket服务实例:

    struct socket_server * ss = socket_server_create();

    这里通过调用 socket_server_create 方法来创建一个 socket_server 对象,这个对象有一个比较重要的成员是 epoll 类型的对象,它负责管理自身所有的socket连接和数据读写操作。

  • test函数:
    test方法中做的事情主要是:

    • 创建和启动一个线程用于轮询socket消息:

      pthread_t pid;
      //创建一个新的线程用于轮询poll消息
      pthread_create(&pid, NULL, _poll, ss);
      //启动轮询线程
      pthread_join(pid, NULL); 
    • 尝试作为一个客户端去连接一个端口地址:

      int c = socket_server_connect(ss,100,"127.0.0.1",80);
    • 作为一个服务器新建一个监听(监听指定端口地址),并启动监听:

      //创建监听,返回socket句柄用于操作此监听
      int l = socket_server_listen(ss,200,"127.0.0.1",8888,32);
      printf("listening %d\n",l);
      //通过操作句柄启动此socket
      socket_server_start(ss,201,l);
    • 创建多个连接请求,等待5秒后退出socket:

      int i;
      for (i=0;i<100;i++) {
          socket_server_connect(ss, 400+i, "127.0.0.1", 8888);
      }
      //休眠5秒
      sleep(5);
      //退出socket
      socket_server_exit(ss);
  • 循环查询socket消息:

    static void * _poll(void * ud) {
        struct socket_server *ss = ud;
        //返回消息的内容
        struct socket_message result;
        //执行一个死循环
        for (;;) {
            //一直查询socket消息类型type
            int type = socket_server_poll(ss, &result, NULL);
            // 最好不要在这个线程中执行对于socket的操作指令 (例如:socket_server_close 等 ) 
            switch (type) {
            case SOCKET_EXIT:
                //退出死循环
                return NULL;
            case SOCKET_DATA:
                //此时result.ud表示的是result.data的大小
                printf("message(%lu) [id=%d] size=%d\n",result.opaque,result.id, result.ud);
                //释放数据缓存
                free(result.data);
                break;
            case SOCKET_CLOSE:
                printf("close(%lu) [id=%d]\n",result.opaque,result.id);
                break;
            case SOCKET_OPEN:
                printf("open(%lu) [id=%d] %s\n",result.opaque,result.id,result.data);
                break;
            case SOCKET_ERROR:
                printf("error(%lu) [id=%d]\n",result.opaque,result.id);
                break;
            case SOCKET_ACCEPT:
                //被动连接,此时还不能发送消息给连接上来的客户端,因为socket还没加入到poll中进行管理,需要先调用socket_server_start才能发送数据
                printf("accept(%lu) [id=%d %s] from [%d]\n",result.opaque, result.ud, result.data, result.id);
                break;
            }
        }
    }

4.socket_server.h源码剖析:

一般阅读源码,肯定是先从.h头文件读起,因为这里定义了此类对外提供的功能接口,所以在解读 socket_server.c 前我们先来看看它的头文件 socket_server.h

  • 首先,宏定义中定义了socket通信的所有消息类型:

     //宏定义了socket_server_poll()返回的socket消息类型
     #define SOCKET_DATA 0      //数据data到来消息
     #define SOCKET_CLOSE 1     //关闭连接消息
     #define SOCKET_OPEN 2      //连接成功消息
     #define SOCKET_ACCEPT 3    //被动连接建立消息(Accept返回了连接的fd句柄,但此连接还未被假如epoll中管理)
     #define SOCKET_ERROR 4     //错误消息
     #define SOCKET_EXIT 5      //退出socket消息
     #define SOCKET_UDP 6       //udp通信消息
  • 消息数据结构:
    在skynet的socket通信的c源码中定义了几个消息结构(socket_messageskynet_socket_messageskynet_message),他们分别对应于不同的服务:

    • socket_message 对应于 socket_server 服务中的消息传输类型:

      struct socket_message {
          int id;             //应用层的socket fd句柄
          uintptr_t opaque;   //在skynet中对应一个Ator实体的handle句柄
          int ud;             //对于accept连接来说, ud是新连接的fd;对于数据(data)来说, ud是数据的大小 
          char * data;        //数据指针
      };
    • 在将socket_server引入到skynet框架中时,还进行了第二次的封装,在skynet中调用socket服务也是调用封装后的 skynet_socket.h 中的接口,skynet_socket_message 对应 skynet_socket_server

      //skynet_socket服务间传递消息结构
      struct skynet_socket_message {
          int type;       
          int id;           //
          int ud;
          char * buffer;    //消息携带数据
      };
    • 当然在skynet不同服务(Actor)间进行通信的时候还使用了另一种消息结构:skynet_message对应 Actor之间,定义在 skynet_mq.h 中:

      struct skynet_message {
          uint32_t source;
          int session;
          void * data;
          size_t sz;
      };
  • 核心API接口:
    假如要在C语言中直接使用socket_server,基本上是用这些封装好的接口基本上也就足够了:

    //创建一个socket_server
    struct socket_server * socket_server_create();
    //释放一个socket_server的资源占用
    void socket_server_release(struct socket_server *);
    /*
    * 封装了的epoll或kqueue,用来获取socket的网络事件或消息
    * (通常放在循环体中持续监听网络消息)
    * socket_server : socket_server_create() 返回的socket_server实例
    * result        : 结果数据存放的地址指针
    *               : 返回消息类型,对应于宏定义中的SOCKET_DATA的类型
    */
    int socket_server_poll(struct socket_server *, struct socket_message *result, int *more);
    //退出socket_server
    void socket_server_exit(struct socket_server *);
    /*  
    * 关闭socket_server
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * id            : socket_server_listen() 返回的id
    */
    void socket_server_close(struct socket_server *, uintptr_t opaque, int id);
    /*  
    * 停止socket
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * id            : socket句柄
    */
    void socket_server_shutdown(struct socket_server *, uintptr_t opaque, int id);
    /*  
    * 启动socket监听(启动之前要先通过socket_server_listen()绑定端口)
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * id            : socket_server_listen() 返回的id
    */
    void socket_server_start(struct socket_server *, uintptr_t opaque, int id);
    /*
    * 发送数据
    * socket_server : socket_server_create() 返回的socket_server实例
    * buffer        : 要发送的数据
    * sz            : 数据的大小
    * id            : socket_server_listen() 返回的id
    *               : 假如返回-1表示error
    */
    int64_t socket_server_send(struct socket_server *, int id, const void * buffer, int sz);
    
    void socket_server_send_lowpriority(struct socket_server *, int id, const void * buffer, int sz);
    /*  
    * 绑定监听ip端口
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * addr          : ip地址
    * port          : 端口号
    *               : 返回一个id作为操作此端口监听的句柄        
    */
    int socket_server_listen(struct socket_server *, uintptr_t opaque, const char * addr, int port, int backlog);
    /*  
    * 以非阻塞的方式连接服务器
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * addr          : ip地址
    * port          : 端口号
    *               : 返回一个id作为操作此端口监听的句柄        
    */
    int socket_server_connect(struct socket_server *, uintptr_t opaque, const char * addr, int port);
    /*  
    * 并不对应bind函数,而是将stdin、stout这类IO加入到epoll中管理
    * socket_server : socket_server_create() 返回的socket_server实例
    * opaque        : skynet中服务handle的句柄
    * fd            : socket的文本描述       
    */
    int socket_server_bind(struct socket_server *, uintptr_t opaque, int fd);
    
    // for tcp
    void socket_server_nodelay(struct socket_server *, int id);
    /*
    * 创建一个udp socket监听,并绑定skynet服务的handle,udp不需要像tcp那样要调用socket_server_start后才能接收消息
    * 如果port != 0, 绑定socket,如果addr == NULL, 绑定 ipv4 0.0.0.0。如果想要使用ipv6,地址使用“::”,端口中port设为0
    */
    int socket_server_udp(struct socket_server *, uintptr_t opaque, const char * addr, int port);
    // 设置默认的端口地址,返回0表示成功
    int socket_server_udp_connect(struct socket_server *, int id, const char * addr, int port);
    /*
    * 假如 socket_udp_address 是空的, 使用最后最后调用 socket_server_udp_connect 时传入的address代替
    * 也可以使用 socket_server_send 来发送udp数据
    */
    int64_t socket_server_udp_send(struct socket_server *, int id, const struct socket_udp_address *, const void *buffer, int sz);
    // 获取传入消息的IP地址 address, 传入的 socket_message * 必须是SOCKET_UDP类型
    const struct socket_udp_address * socket_server_udp_address(struct socket_server *, struct socket_message *, int *addrsz);
    
    // if you send package sz == -1, use soi.
    void socket_server_userobject(struct socket_server *, struct socket_object_interface *soi);

5.socket_server.c源码解析:

首先,我们先看一下声明的几个结构体:

//写缓冲队列
struct wb_list {
    struct write_buffer * head; //写缓冲区的头指针
    struct write_buffer * tail; //写缓冲区的尾指针
};
struct socket {
    uintptr_t opaque;   //所属服务在skynet中对应的handle
    struct wb_list high;//高优先级写队列
    struct wb_list low; //低优先级写队列
    int64_t wb_size;    //写缓存大小
    int fd;             //对应内存分配的fd(文件描述)
    int id;             //应用层维护一个与fd对应的id句柄
    uint16_t protocol;  //使用的协议类型(TCP/UDP)
    uint16_t type;      //scoket的类型或状态(读、写、监听等)
    union {
        int size;       //读缓存预估需要的大小
        uint8_t udp_address[UDP_ADDRESS_SIZE];
    } p;
};
struct socket_server {
    int recvctrl_fd;    //接收管道的句柄
    int sendctrl_fd;    //发送管道的句柄
    int checkctrl;      //释放检测命令
    poll_fd event_fd;   //epoll或kevent的句柄
    int alloc_id;       //应用层分配id用的
    int event_n;        //epoll_wait 返回的事件数
    int event_index;    //当前处理的事件序号
    struct socket_object_interface soi;
    struct event ev[MAX_EVENT];     //epoll_wait 返回的事件集合
    struct socket slot[MAX_SOCKET]; //每个Socket server可以包含多个Socket,这是存储这些Socket的数组(应用层预先分配的)
    char buffer[MAX_INFO];          //临时数据的保存,比如保存对方的地址信息等
    uint8_t udpbuffer[MAX_UDP_PACKAGE];
    fd_set rfds;        //用于select的fd集
};

6.socket_server接口实现分析:

  • 调用过程:
    一个socket_server中可以有多个socket,工作实现过程如下:skynet的某个服务通过 Socket_Server 发送命令来操作底层的Socket

  • 原理解析:
    服务调用socket_server的任何接口,实质上都是通过向socket_server管道的写端发送一个 request_package 的命令包,例如:

int socket_server_connect(struct socket_server *ss, uintptr_t opaque, const char * addr, int port) {
    //创建一个命令结构体
    struct request_package request;
    //计算包体的大小
    int len = open_request(ss, &request, opaque, addr, port);
    //包体小于0则不执行此操作
    if (len < 0)
        return -1;
    //向写管道发送一个'0'指令,发起一个tcp连接请求
    send_request(ss, &request, 'O', sizeof(request.u.open) + len);
    //返回一个用于操作此socket连接的句柄
    return request.u.open.id;
}
  • 常用操作指令:
    上面发起连接时发送了一个‘O’(字母)指令来实现Socket连接操作请求,其实socket_server其他操作的实现方式也与此类似,只是使用的指令不同,常用的指令有:
    • S Start socket 启动一个Socket
    • B Bind socket 绑定一个Socket
    • L Listen socket 监听一个Socket
    • K Close socket 关闭一个Socket
    • O Connect to (Open) 连接一个Socket
    • X Exit 退出一个Socket
    • D Send package (high) 发送数据
    • P Send package (low) (不常用,也用于发送数据)
    • A Send UDP package
    • T Set opt
    • U Create UDP socket
    • C set udp address

以上就是socket_server的源码核心部分,这部分集成到skynet的C API中后,采用异步读写,直接通过C语言调用的话,监听一个端口或者发起一个TCP连接,操作的结果要等待skynet的事件回调,skynet会将结果以PTYPE_SOCKET类型的消息发送给发起请求的服务。


skynet中Socket服务的使用:

接下来,我们直接使用skynet框架中自带的socket服务,为了进一步适用于Skynet框架,又进行一步对socket_server进行了封装,所有常用的接口都封装在 skynet_socket.hskynet_socket.c 中,因为框架业务层逻辑都使用lua来编写,所以下面我们尝试在lua中启动一个socket服务。

1.注意点:

由于异步非阻塞的C API在skynet中使用起来并不方便,结合lua的语言特性中的coroutine机制(携程),skynet中采用阻塞模式封装了一组lua API用于TCP Socket的读写操作。

当我们在skynet的某个服务的lua中调用了Socket api时,服务有可能被挂起(时间片被让给其他业务处理),待结果通过socket消息返回,coroutine将延续执行。

2.API

几个常用的skynet中的Socket接口:

*   新建一个TCP连接:        socket.open(address, port)
*   启动socket监听:        socket.start(id)
*   读取socket接收数据:    socket.read(id)
*   向socket中写数据:      socket.write(id, str)
*   监听一个端口:          socket.listen(id, port)
*   服务开始方式:          socket.abandon(id)
*   关闭socketsocket.close(id)

查询接口可以在lualib/socket.lua中查找,更详细的API解析参考:skynet官方文档

  • socket.open(address, port) 建立一个 TCP 连接。返回一个数字 id 。
  • socket.close(id) 关闭一个连接,这个 API 有可能阻塞住执行流。因为如果有其它 coroutine 正在阻塞读这个 id 对应的连接,会先驱使读操作结束,close 操作才返回。
  • socket.close_fd(id) 在极其罕见的情况下,需要粗暴的直接关闭某个连接,而避免 socket.close 的阻塞等待流程,可以使用它。
  • socket.shutdown(id) 强行关闭一个连接。和 close 不同的是,它不会等待可能存在的其它 coroutine 的读操作。一般不建议使用这个 API ,但如果你需要在 __gc 元方法中关闭连接的话,shutdown 是一个比 close 更好的选择(因为在 gc 过程中无法切换 coroutine)。
  • socket.read(id, sz) 从一个 socket 上读 sz 指定的字节数。如果读到了指定长度的字符串,它把这个字符串返回。如果连接断开导致字节数不够,将返回一个 false 加上读到的字符串。如果 sz 为 nil ,则返回尽可能多的字节数,但至少读一个字节(若无新数据,会阻塞)。
  • socket.readall(id) 从一个 socket 上读所有的数据,直到 socket 主动断开,或在其它 coroutine 用 socket.close 关闭它。
  • socket.readline(id, sep) 从一个 socket 上读一行数据。sep 指行分割符。默认的 sep 为 “\n”。读到的字符串是不包含这个分割符的。
  • socket.block(id) 等待一个 socket 可读。

socket api 中有两个不同的写操作。对应 skynet 为每个 socket 设定的两个写队列。通常我们只需要用:

  • socket.write(id, str) 把一个字符串置入正常的写队列,skynet 框架会在 socket 可写时发送它。
    但同时 skynet 还提供一个低优先级的写操作(如果你不需要这个设计,可以不使用它):

  • socket.lwrite(id, str) 把字符串写入低优先级队列。如果正常的写队列还有写操作未完成时,低优先级队列上的数据永远不会被发出。只有在正常写队列为空时,才会处理低优先级队列。但是,每次写的字符串都可以看成原子操作。不会只发送一半,然后转去发送正常写队列的数据。

对于服务器,通常我们需要监听一个端口,并转发某个接入连接的处理权。那么可以用如下 API :

  • socket.listen(address, port) 监听一个端口,返回一个 id ,供 start 使用。
  • socket.start(id , accept) accept 是一个函数。每当一个监听的 id 对应的 socket 上有连接接入的时候,都会调用 accept 函数。这个函数会得到接入连接的 id 以及 ip 地址。你可以做后续操作。

每当 accept 函数获得一个新的 socket id 后,并不会立即收到这个 socket 上的数据。这是因为,我们有时会希望把这个 socket 的操作权转让给别的服务去处理。

socket 的 id 对于整个 skynet 节点都是公开的。也就是说,你可以把 id 这个数字通过消息发送给其它服务,其他服务也可以去操作它。任何一个服务只有在调用 socket.start(id) 之后,才可以收到这个 socket 上的数据。skynet 框架是根据调用 start 这个 api 的位置来决定把对应 socket 上的数据转发到哪里去的

向一个 socket id 写数据也需要先调用 start ,但写数据不限制在调用 start 的同一个服务中。也就是说,你可以在一个服务中调用 start ,然后在另一个服务中向其写入数据。skynet 可以保证一次 write 调用的原子性。即,如果你有多个服务同时向一个 socket id 写数据,每个写操作的串不会被分割开。

  • socket.abandon(id) 清除 socket id 在本服务内的数据结构,但并不关闭这个 socket 。这可以用于你把 id 发送给其它服务,以转交 socket 的控制权。

  • socket.warning(id, callback) 当 id 对应的 socket 上待发的数据超过 1M 字节后,系统将回调 callback 以示警告。function callback(id, size) 回调函数接收两个参数 id 和 size ,size 的单位是 K 。如果你不设回调,那么将每增加 64K 利用 skynet.error 写一行错误信息。

3.服务端实现:

参考skynet提供的testsocket.lua的实现案例,创建一个socket服务端,接收客户端的连接,输出连接客户端的IP地址,并发送一段字符串”hello,”,步骤如下:

  • 将Ip和端口号等信息添加到config配置文件中:

    server_ip  = "0.0.0.0:10080"
  • 新建一个服务器脚本socketserver.lua放在testexamples目录下;
  • 引入模块并创建skynet服务实例和socket实例:

    local skynet    = require "skynet"
    local socket    = require "socket"
  • 调用skynet.start接口,并设置监听指定ip地址:
    官方的案例提供了两种方式用于socket处理,普通模式agent模式,他们的实现方式分别如下:

    • 普通模式:

      local function accept(id)
          socket.start(id)
          --向socket中写数据
          socket.write(id, "Hello Skynet\n")
          --创建一个agent服务
          skynet.newservice(SERVICE_NAME, "agent", id)
          -- notice: Some data on this connection(id) may lost before new service start.
          -- So, be careful when you want to use start / abandon / start .
          socket.abandon(id)
      end
      skynet.start(function()
          --监听一个端口,返回的id可作为此socket的句柄,用来操作此socket
          local id = assert(socket.listen(skynet.getenv "server_ip"))
          print("Listen socket :"..skynet.getenv "server_ip")
          --启动socket监听
          socket.start(id , function(id, addr)
              print("connect from " .. addr .. " " .. id)
              -- you have choices :
              -- 1. skynet.newservice("testsocket", "agent", id)
              -- 2. skynet.fork(echo, id)
              -- 3. accept(id)
              --处理接收的数据的方法
              accept(id)
          end)
      end)
    • agent模式:

      local function echo(id)
          socket.start(id)
          while true do
              local str = socket.read(id)
              if str then
                  socket.write(id, str)
              else
                  socket.close(id)
                  return
              end
          end
      end
      id = tonumber(id)
      skynet.start(function()
          --创建一个额外线程用于监听返回结果
          skynet.fork(function()
              echo(id)
              skynet.exit()
          end)
      end)
  • 参考这个例子,我们编写我们的服务端脚本代码如下:

    local skynet    = require "skynet"
    local socket    = require "socket"
    
    --简单echo服务
    function    echo(id, addr)
        socket.start(id)
        while true do
            local str = socket.read(id)
            if str then
                skynet.error("客户端"..id, " 发送内容: ", str)
                socket.write(id, str)
            else
                socket.close(id)
                skynet.error("客户端"..id, " ["..addr.."]", "断开连接")
                return
            end
        end
    end
    
    --服务入口
    skynet.start(function()
        local id    = assert(socket.listen(skynet.getenv "app_server"))
        socket.start(id, function(id, addr)
            skynet.error("客户端"..id, " ["..addr.."]", "已连接")
            skynet.fork(echo, id, addr)
        end)
    end)

4.客户端实现:

客户端的功能是向服务器监听的端口发送字符串数据,然后打印输出服务器返回的数据。

  • 新建一个socket客户端测试脚本socketclient.lua放在testexamples目录下,内容如下:

    local skynet    = require "skynet"
    local socket    = require "socket"
    
    local name = ... or ""
    
    function _read(id)
        while true do
            local str   = socket.read(id)
            if str then
                skynet.error(id, "收到服务器数据: ", str)
                socket.close(id)
                skynet.exit()
            else
                socket.close(id)
                skynet.error("断开链接")
                skynet.exit()
            end
        end
    end
    
    skynet.start(function()
        --连接到服务器
        local addr  = skynet.getenv "app_server"
        local id    = socket.open(addr)
        if not id then
            skynet.error("无法连接 "..addr)
            skynet.exit()
        end
    
        skynet.error("已连接")
    
        --启动读协程
        skynet.fork(_read, id)
    
        socket.write(id, "hello, "..name)
    end)

运行程序:

  • 便捷启动skynet的lua服务:
    这里使用了另外一种启动snlua服务的方式:

    linsh@ubuntu:/application/skynet$ ./skynet config 
    [:01000001] LAUNCH logger 
    [:01000002] LAUNCH snlua bootstrap
    [:01000003] LAUNCH snlua launcher
    [:01000004] LAUNCH snlua cmaster
    [:01000004] master listen socket 0.0.0.0:2017
    [:01000005] LAUNCH snlua cslave
    [:01000005] slave connect to master 127.0.0.1:2017
    [:01000006] LAUNCH harbor 1 16777221
    [:01000004] connect from 127.0.0.1:59156 4
    [:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526
    [:01000005] Waiting for 0 harbors
    [:01000005] Shakehand ready
    [:01000007] LAUNCH snlua datacenterd
    [:01000008] LAUNCH snlua service_mgr
    [:01000009] LAUNCH snlua main
    [:01000009] Server start
    [:0100000a] LAUNCH snlua protoloader
    [:0100000b] LAUNCH snlua console
    [:0100000c] LAUNCH snlua debug_console 8000
    [:0100000c] Start debug console at 127.0.0.1:8000
    [:0100000d] LAUNCH snlua simpledb
    [:0100000e] LAUNCH snlua watchdog
    [:0100000f] LAUNCH snlua gate
    [:0100000f] Listen on 0.0.0.0:8888
    [:01000009] Watchdog listen on 8888
    [:01000009] KILL self
    [:01000002] KILL self
    

    也就是先使用.skynet (config文件目录)来启动skynet服务,然后输入要启动的lua服务名称,例如这里我们创建的firsttest.lua

    firsttest
    [:01000010] LAUNCH snlua firsttest

    如此便可以实现启动指定的服务,无需通过修改main.lua来完成lua服务的启动。

  • 启动服务器:
    在终端输入:

    socketserver
  • 启动客户端:
    在终端输入:

    socketclient
  • 输出结果:

    linsh@ubuntu:/application/skynet$ ./skynet config
    [:01000001] LAUNCH logger 
    [:01000002] LAUNCH snlua bootstrap
    [:01000003] LAUNCH snlua launcher
    [:01000004] LAUNCH snlua cmaster
    [:01000004] master listen socket 0.0.0.0:2017
    [:01000005] LAUNCH snlua cslave
    [:01000005] slave connect to master 127.0.0.1:2017
    [:01000004] connect from 127.0.0.1:59202 4
    [:01000006] LAUNCH harbor 1 16777221
    [:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526
    [:01000005] Waiting for 0 harbors
    [:01000005] Shakehand ready
    [:01000007] LAUNCH snlua datacenterd
    [:01000008] LAUNCH snlua service_mgr
    [:01000009] LAUNCH snlua main
    [:01000009] Server start
    [:0100000a] LAUNCH snlua protoloader
    [:0100000b] LAUNCH snlua console
    [:0100000c] LAUNCH snlua debug_console 8000
    [:0100000c] Start debug console at 127.0.0.1:8000
    [:0100000d] LAUNCH snlua simpledb
    [:0100000e] LAUNCH snlua watchdog
    [:0100000f] LAUNCH snlua gate
    [:0100000f] Listen on 0.0.0.0:8888
    [:01000009] Watchdog listen on 8888
    [:01000009] KILL self
    [:01000002] KILL self
    socketserver
    [:01000010] LAUNCH snlua socketserver
    socketclient
    [:01000012] LAUNCH snlua socketclient
    [:01000010] 客户端10  [127.0.0.1:54504] 已连接
    [:01000012] 已连接
    [:01000010] 客户端10  发送内容:  hello, 
    [:01000012] 9 收到服务器数据:  hello, 
    [:01000010] 客户端10  [127.0.0.1:54504] 断开连接
    [:01000012] KILL self

    我们看[]符号中间的id,那个就是skynet用来管理服务所分配的id标识,不难发现socketserver服务的id是10,而socketclient服务的id是12,客户端启动成功后连接服务器,服务器接收到连接打印当前请求连接的客户端IP,同时返回一个字符串“hello, ”然后客户端收到连接成功的结果,打印出“已连接”和服务器发送的数据,然后服务器就断开了与客户端的连接。


小结:

本篇主要是了解Socket_Server这个Socke的C API的实现过程还有引入到Skynet框架后的一些调整,最后其实重要的是了解 Socket_Server的lua API 的使用以及 Lua的Coroutine机制


附件:

关于socket_server源码解析,还可以参考一下视频:


参考:

文章最后发布于: 2017-05-22 19:43:19
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 书香水墨 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览