問題描述
在使用 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;
}