首頁>技術>

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)

假如是在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.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 中定義

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_serverstruct 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_servervoid 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 tcpvoid 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 啟動一個SocketB Bind socket 繫結一個SocketL Listen socket 監聽一個SocketK Close socket 關閉一個SocketO Connect to (Open) 連線一個SocketX Exit 退出一個SocketD Send package (high) 傳送資料P Send package (low) (不常用,也用於傳送資料)A Send UDP packageT Set optU Create UDP socketC 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)endskynet.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    endendid = 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    endend--服務入口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    endendskynet.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 selfsocketserver[:01000010] LAUNCH snlua socketserversocketclient[: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機制

6
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 演算法面試真題詳解:搜尋二維