epoll 的原理不费劲叙述了, 如果未来有时间再写吧.
说说网上找到的代码的问题
-
只监听 socket 可读, 读完就写而没有检查 socket 是否可写. 虽然一般的确 socket 可写, 但是仍然应该检查, 就算是 epoll 教学程序也应当检查.
-
读写倒是分开的, 但是所有连接都用一个
buf
, 导致竞争.
代码如下.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <assert.h>
#define CHECK_RET(ret, errstr) \
if ((ret) == -1) { \
perror(errstr); exit(1); \
}
#define LISTEN_IP "0.0.0.0"
#define LISTEN_PORT 8080
#define LISTEN_BACKLOG 3
#define NUM_EVENTS 10000
#define BUNDLE_BUFSZ 256
struct conn_bundle {
char buf[BUNDLE_BUFSZ];
int bufsz;
int fd;
};
int listen_on(const char* ip, int port) {
// create socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
CHECK_RET(sockfd, "socket");
// create sockaddr
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
// set sockaddr reuse
int ena = 1;
int err = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &ena, sizeof(ena));
CHECK_RET(err, "setsockopt(SO_REUSEADDR)");
// bind socket to addr
err = bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));
CHECK_RET(err, "bind");
// listen
err = listen(sockfd, LISTEN_BACKLOG);
CHECK_RET(err, "listen");
return sockfd;
}
void handle_events(struct epoll_event* events, int n_events,
int listenfd, int efd) {
for (int i = 0; i < n_events; i++) {
struct epoll_event* ev = events + i;
struct conn_bundle* bundle = (struct conn_bundle*) ev->data.ptr;
if (bundle->fd == listenfd) {
if (ev->events & EPOLLIN) {
// new incoming conn, get connfd
int connfd = accept(listenfd, NULL, NULL);
if (connfd == -1) continue; // ignore error
// create event
struct conn_bundle* bundle = malloc(sizeof(struct conn_bundle));
memset(bundle, 'x', sizeof(struct conn_bundle));
if (bundle == NULL) { // allocation failure, don't serve client
close(connfd);
free(bundle);
printf("> refuse to serve client\n");
} else {
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.ptr = bundle;
bundle->fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
printf("> got new client\n");
}
} // else: exception happened on listenfd. Now ignore that.
continue; // exclusive with latter case
}
if (ev->events & EPOLLIN) { // some connfd readable
// XXX: don't consider partial reads
int n_read = read(bundle->fd, bundle->buf, BUNDLE_BUFSZ);
if (n_read == 0) {
printf("> client premature close\n");
free(ev->data.ptr);
epoll_ctl(efd, EPOLL_CTL_DEL, bundle->fd, NULL);
} else if (n_read > 0) {
bundle->bufsz = n_read;
for (int i = 0; i < bundle->bufsz; i++) putchar(bundle->buf[i]);
printf("> hexdump:\n");
for (int i = 0; i < bundle->bufsz; i++) printf("%02x ", bundle->buf[i]);
printf("\n");
fflush(stdout);
// now let epoll monitor for write
ev->events = EPOLLOUT | EPOLLET;
epoll_ctl(efd, EPOLL_CTL_MOD, bundle->fd, ev);
} else { // error
close(bundle->fd);
free(bundle);
}
}
if (ev->events & EPOLLOUT) {
// XXX: don't consider partial write
write(bundle->fd, bundle->buf, bundle->bufsz);
close(bundle->fd);
free(bundle);
printf("> finish client\n");
}
}
}
void main_loop(int listenfd) {
// epoll setup
int efd = epoll_create(1); // argument is ignored
CHECK_RET(efd, "epoll_create");
// monitor listenfd to become readable
struct epoll_event event;
event.events = EPOLLIN; // don't use edge trigger for listenfd
event.data.ptr = malloc(sizeof(struct conn_bundle));
struct conn_bundle* bundle = (struct conn_bundle*) event.data.ptr;
bundle->fd = listenfd;
bundle->bufsz = 0;
epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &event);
// main loop
while (1) {
struct epoll_event events[NUM_EVENTS];
int n_events = epoll_wait(efd, events, NUM_EVENTS, -1);
if (n_events == -1 && errno == EINTR) continue;
CHECK_RET(n_events, "epoll_wait"); // No possible error is expected here
handle_events(events, n_events, listenfd, efd);
}
}
int main() {
int listenfd = listen_on(LISTEN_IP, LISTEN_PORT);
main_loop(listenfd);
}
最后发现一个 telnet 的有意思的情况. 同样是做 TCP 连接, telnet 在我按下回车的时候发送的是 CR LF, 而 nc 只发送 LF. 并且要想让 telnet 只发送 LF 很难, 无法调整而且也不能用二进制发送. 我想古代人用 telnet 的时候也没想到连到的对面不是一个电传打字机的情况吧.
最大的感受就是, C 写网络编程真的麻烦, bind 一个 ip 都要弄好几行.