skynet clientsocket 導致 io.read 無法正確工作的問題

問題描述

在使用 clientsocket 之後,使用 io.read() 無法正確讀取用戶輸入。

issue 裏的討論

關於這個問題,在 skynet 的 issue 裏也有人提到:
require “clientsocket” 後 io.read()行爲改變 #539

io.read()等待讀取一行命令行的輸入,以回車結束;
require “clientsocket”後,io.read() 需要先按回車,然後輸入,再按兩次回車,輸入纔會生效。
請問這是怎麼回事?

--測試代碼
package.cpath = "./luaclib/?.so;"
--local socket = require "clientsocket"
local CMD = {}
local loop_flag = true

function CMD.exit(...)
print("reach the end...")
loop_flag = false
end

local function start(cmd, ...)
local f = assert(CMD[cmd], "command " .. cmd .. " not found")
f(...)
end

while loop_flag do
local cmd = io.read()
local ok, status = pcall(start, cmd)
if not ok then
print(status)
end
end
--------------------------測試結果---------------------
註釋require "clientsocket"前:
dongsongtao:skynet ((v1.0.0-rc5))$ lua examples/loop_test.lua
exit(回車)
(回車)
(回車)
examples/loop_test.lua:12: command not found
(回車)
exit(回車)
(回車)
reach the end...
註釋require "clientsocket"後:
dongsongtao:skynet ((v1.0.0-rc5))$ lua examples/loop_test.lua
exit(回車)
reach the end...

對此雲風的回覆是:

不要使用 clientsocket , 僅用於 examples 。請使用更成熟的 socket 庫或自己實現。

問題原因

其實很簡單,看下代碼就知道爲什麼會這樣了, 代碼位於 lua-clientsocket.c 內。

原因就是在這個庫裏,雲風簡單的實現了一個非阻塞的io.read接口: socket.readline() , 這個函數可以讀取用戶的輸入,並且不阻塞當前協程。

這個函數的原理:
1. 創建一個線程,不斷的從 stdin 內讀取輸入,並把讀取到的內容保存到一個隊列中。
2. lua 層,每次調用 socket.readline() 接口,從上述隊列中取出一條用戶輸入。

由此不難發現 io.read 不能正確工作的原因,因爲所有的輸入都被readline_stdin線程給“搶”去了,通過 lua 的 io.read 自然不可能得到任何結果。

// quick and dirty none block stdin readline

#define QUEUE_SIZE 1024

// 存放用戶輸入的隊列
struct queue {
    pthread_mutex_t lock;
    int head;
    int tail;
    char * queue[QUEUE_SIZE];
};

// 循環讀取用戶輸入的線程執行函數
static void *
readline_stdin(void * arg) {
    struct queue * q = arg;
    char tmp[1024];
    while (!feof(stdin)) {
        if (fgets(tmp,sizeof(tmp),stdin) == NULL) {
            // read stdin failed
            exit(1);
        }
        int n = strlen(tmp) -1;

        char * str = malloc(n+1);
        memcpy(str, tmp, n);
        str[n] = 0;

        pthread_mutex_lock(&q->lock);
        q->queue[q->tail] = str;

        if (++q->tail >= QUEUE_SIZE) {
            q->tail = 0;
        }
        if (q->head == q->tail) {
            // queue overflow
            exit(1);
        }
        pthread_mutex_unlock(&q->lock);
    }
    return NULL;
}

// socket.readstdin()
static int
lreadstdin(lua_State *L) {
    struct queue *q = lua_touserdata(L, lua_upvalueindex(1));
    pthread_mutex_lock(&q->lock);
    if (q->head == q->tail) {
        pthread_mutex_unlock(&q->lock);
        return 0;
    }
    char * str = q->queue[q->head];
    if (++q->head >= QUEUE_SIZE) {
        q->head = 0;
    }
    pthread_mutex_unlock(&q->lock);
    lua_pushstring(L, str);
    free(str);
    return 1;
}

如何解決?

知道了問題的原因,解決起來就太簡單了。

1. 不改 lua-clientsocket.c 源碼

不要直接使用 io.read(), 而是改爲使用 socket.readstdin() , 如果還是想要“阻塞”,可以使用 socket.usleep() .

使用範例:

skynet自身帶了一個client demo,在 examples/client.lua 內,這個裏面已經給出了這種用法。

while true do
    dispatch_package()
    local cmd = socket.readstdin()
    if cmd then
        if cmd == "quit" then
            send_request("quit")
        else
            send_request("get", { what = cmd })
        end
    else
        socket.usleep(100)
    end
end

2. 修改源碼

如果就是想用 io.read(), 並且也不想用非阻塞的IO, 可以把創建線程的代碼註釋掉,就不會把 stdin 的輸入全吞掉了。

LUAMOD_API int
luaopen_client_socket(lua_State *L) {
    luaL_checkversion(L);
    luaL_Reg l[] = {
        { "connect", lconnect },
        { "recv", lrecv },
        { "send", lsend },
        { "close", lclose },
        { "usleep", lusleep },
        { NULL, NULL },
    };
    luaL_newlib(L, l);

    /*struct queue * q = lua_newuserdata(L, sizeof(*q));
    memset(q, 0, sizeof(*q));
    pthread_mutex_init(&q->lock, NULL);
    lua_pushcclosure(L, lreadstdin, 1);
    lua_setfield(L, -2, "readstdin");

    pthread_t pid ;
    pthread_create(&pid, NULL, readline_stdin, q);*/

    return 1;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章