socket_server C源码解析:
较早版本的skynet并没有提供对于网络层的定制,而是可以由开发者自行定义,不过最新版本的skynet已经加入了网络层的支持,也有独立的项目例子socket-server是纯C语言的实现。核心源码包括: socket_epoll.h 、 socket_kqueue.h 、 socket_poll.h 、 socket_server.c 和 socket_server.h 。
1.异步IO
此网络库已经封装了socket的 epoll 和 kququ 两种底层接口,其功能就是: 处理阻塞/非阻塞socket中 read/write 的问题 ,它们的区别在于适用于不同 操作系统 的通信接口的封装:
- epoll ( Linux2.6下性能最好的多路I/O就绪通知方法 ) -> Linux系统
- kqueue -> 其他Unix的变种系统(例如: FreeBSD )
这一点可以从 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.c 和 socket_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.h 和 socket_kqueue.h 中定义
【文章福利】文章总是那么的枯燥无味不易理解,想学习更多skynet服务器框架知识的小伙伴可以 后台私信“skynet” 获取直播地址,将在12.30日-12.31日两天进行直播讲解skynet框架,帮助你完全掌握skynet服务器框架
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);
在网上检索资料了解到原因大致如下:
- 在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_message 、 skynet_socket_message 和 skynet_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.h 和 skynet_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)
* 关闭socket: socket.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 放在 test 或 examples 目录下;
引入模块并创建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 放在 test 或 examples 目录下,内容如下:
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机制 。
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~