In my recent work on websocket porting, we need to implement the underlying socket read and write. I have written a set based on synchronous read, libevent, libuv, and Android logoff, and I have learned a lot from it.
1) Synchronous read/write Blocking
At first, synchronous blocking reading and writing was adopted to quickly verify the completeness of the Upper-layer websocket protocol. The advantage is that it is easy to implement. The disadvantage is that the efficiency is not high and the thread resources cannot be used well. The connection establishment method is similar. The main difference is how to read and write data, let's take a look at the one shared by several methods:
int n = 0; struct sockaddr_in serv_addr; event_init(); if((mSockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ //TODO error return; } memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(url.port()); if(inet_pton(AF_INET, url.host().utf8().data(), &serv_addr.sin_addr)<=0){ return; } if( connect(mSockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){ return; }
Because it is a client, it is relatively simple. Of course, DNS resolution is missing. Then, we need to monitor the read data. Because the read is blocked synchronously, We need to continuously read/Recv in the Loop:
while (1) { ssize_t result = recv(fd, buf, sizeof(buf), 0); if (result == 0) { break; } else if (result < 0) { perror("recv"); close(fd); return 1; } fwrite(buf, 1, result, stdout); }
The disadvantage is obvious. This thread needs to be polling continuously. Of course, this is a subprogram, and it will not be so hasty in the formal code.
2) libevent
The above improvement method is to process read data in asynchronous non-blocking mode. in Linux, epoll is generally used for asynchronous event listening, libevent is a C library that encapsulates asynchronous events on epoll or other platforms. Therefore, it is easier to Implement Asynchronous non-blocking read/write based on libevent and cross-platform. The first step of refactoring is to set socketfd to non-blocking:
static int setnonblock(int fd){ int flags; flags = fcntl(fd, F_GETFL); if (flags < 0){ return flags; } flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) < 0){ return -1; } return 0;}
Then you need to maintain the event loop in a separate thread and add the read event listener:
static void* loopListen(void *arg){ SocketStreamHandle *handle = (SocketStreamHandle *)arg; struct event_base* base = event_base_new(); struct event ev_read; handle->setReadEvent(&ev_read); setnonblock(handle->getSocketFD()); event_set(&ev_read, handle->getSocketFD(), EV_READ|EV_PERSIST, onRead, handle); event_base_set(base, &ev_read); event_add(&ev_read, NULL); event_base_dispatch(base);}
pthread_t pid; pthread_create(&pid, 0, loopListen, this);
Then process the data read in the onread method:
static void onRead(int fd, short ev, void *arg){ while(true){ char *buf = new char[1024]; memset(buf, 0, 1024); int len = read(fd, buf, 1024); SocketStreamHandle *handle = (SocketStreamHandle *)arg; if(len > 0){ SocketContext *context = new SocketContext; context->buf = buf; context->readLen = len; context->handle = handle; WTF::callOnMainThread(onReadMainThread, context); if(len == 1024){ continue; }else{ break; } }else{ if(errno == EAGAIN || errno == EWOULDBLOCK){ return; }else if(errno == EINTR){ continue; } __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "onCloseMainThread, len:%d, errno:%d", len, errno); WTF::callOnMainThread(onCloseMainThread, handle); event_del(handle->getReadEvent()); } }}
Here we pay more attention to the following:
1) when a Buf cannot be fully read, it needs to be read again in a loop.
2) When read to 0, it indicates that the socket is closed. In this case, you need to delete the event listening. Otherwise, the CPU will be 100%
3) When read to-1, it is not completely an error. For example, errno = eagain | errno = ewouldblock indicates that it is temporarily unreadable and will be read later. Errno = eintr indicates the system was interrupted and should be re-viewed
4) onread is called by a thread dedicated to event listening by libevent, so sometimes it is necessary to return to the main thread, such as: WTF: callonmainthread (onreadmainthread, context ); pay attention to the synchronization between multiple threads.
3) libuv
Libuv goes further in libevent. It not only has event loop, but also covers various socket operations, so the code will be more concise, such as the initial connection creation and loop creation:
uv_loop_t *loop = uv_default_loop(); uv_tcp_t client; uv_tcp_init(loop, &client); struct sockaddr_in req_addr = uv_ip4_addr(url.host().utf8().data(), url.port()); uv_connect_t *connect_req; connect_req->data = this; uv_tcp_connect(connect_req, &client, req_addr, on_connect); uv_run(loop);
Create a read listener in on_connect:
static void* on_connect(uv_connect_t *req, int status){ SocketStreamHandle *handle = (SocketStreamHandle *)arg; uv_read_start(req->handle, alloc_buffer, on_read);}
On_read is similar to the previous one. Therefore, libuv is the most powerful, greatly omitting socket-related development.
4) Android Logoff
Android provides an event Loop Mechanism and can listen to FD. Therefore, if you use Android logoff, you can skip the dependency on third-party Lib. In addition, Android is also an encapsulation of epoll. In this case, it is worth trying to use the android native looper for event logoff. The socket connection is the same as that at the beginning. The key is to create a Logoff:
static void* loopListen(void *arg){ SocketStreamHandle *handle = (SocketStreamHandle *)arg; setnonblock(handle->getSocketFD()); Looper *looper = new Looper(true); looper->addFd(handle->getSocketFD(), 0, ALOOPER_EVENT_INPUT, onRead, handle); while(true){ if(looper->pollOnce(100) == ALOOPER_POLL_ERROR){ __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "ALOOPER_POLL_ERROR"); break; } }}
The code is simple and can be used in detail to view the <utils/lols. h> API.
To sum up, if it is implemented on Android, it can be directly based on the native logoff, and cross-platform can be based on libuv. In short, avoid synchronization blocking, because this will lead to complicated and inefficient thread design.
There are similar concepts in Java. For more information, see the previous blog:
Extract the classic NiO architecture network server from jetty, tomcat, and Mina (1)
Extract the classic NiO architecture network server from jetty, tomcat, and Mina (2)
Extract the classic NiO architecture network server from jetty, tomcat, and Mina (3)