浅谈Linux网络编程(以一个http server为例)
Socket小知识
创建socket要三个参数,
第一个是它的domain:一般来说是AF_INET或者AF_UNIX。前者可以连internet,后者连本地unix文件系统里的socket。前者的address是sockaddr_in,后者的address是sockaddr_un.
第二个是它的数据类型:一般来说是SOCK_STREAM或者SOCK_DGRAM。众所周知TCP是字节流(stream),UDP是报文(DataGRAM)。所以分别对应了两个socket type。
第三个是protocol类型,一般置0,表示自动选择。
服务端基本流程
先自己搞个socket(参数和上面一样)。socket()
再给socket取个名字(bind一个标识符,like ip地址+port)。bind()
这个时候就可以listen()
了。我之前一直理解错了,以为listen()
会block。其实是不会的,它的唯一作用就是标记socket为“passive”(即用来监听),并且建立一个监听队列。超过队列容量的请求会refuse掉。
真正建立连接是之后的accept()
。这个时候如果没有要接收的新连接,才会block。
server v1: AF_UNIX
如下定义socket和server_address。
sockaddr_un server_address;
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, “my_server”);
连接建立之后会在当前目录创一个my_server文件。也印证了AF_UNIX
domain是UNIX本地文件系统相关。
server v2: AF_INET
只是server_address定义不太一样了。
sockaddr_in addr;
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_addr()
可以方便地将ip字符串转化为32位无符号整数。
htons()
是关于byte order的。它将host的byte-order转换为网络中的byte-order。比如x86是little-endian,但是网络是big-endian.
可以用netstats -A inet -n
查看所有的tcp/udp连接。在刚刚启动完client之后看,会有一个TIME_WAIT状态的连接,这就是我们刚才的连接。可以看到port是8080,和我们设置的一样。但是如果不加htons()
的话就会变成另外一个port。
server v3: multi-connection
考虑新建一个子进程来处理accept之后的流程(也是工作最多的流程),主进程直接继续listen。
signal(SIGCHLD, SIG_IGN);
while (true) {
printf("server waiting\n");
// listen does NOT block.
listen(sock_fd, 5);
sockaddr client_addr;
socklen_t client_addr_len;
// accept BLOCKS if there's no new connections.
int client_fd = accept(sock_fd, &client_addr, &client_addr_len);
if (fork() == 0) {
sleep(5);
char out[] = "haha";
write(client_fd, out, sizeof(out));
close(client_fd);
exit(0);
}
else close(client_fd);
}
其实用pthread也行,但懒得搞了。
server v4: select
所谓select,就是监听一堆fd,如果其中有fd是readable / writable / exception的话就通知你。
可以通过fd_set (一个bitset)来指定你要监听哪些fd。
在下面的程序中,我们先监听sock_fd(服务器socket)。
- 当有新连接请求时,sock_fd会触发readable。这时accept并且将新的client_fd加入监听。
- 当client_fd触发readable的时候说明client传来了信息。
- 信息可能是EOF。这时read不到(nread == 0)。那就可以移除对该client_fd的监听,并且关闭连接。
- 信息不是EOF。转入正常处理逻辑。
这里还有一个点:select函数是会block的,直到有事件到来。当然,你也可以设置block的timeout。
fd_set fds, test_fds;
FD_ZERO(&fds);
FD_SET(sock_fd, &fds);
while (true) {
test_fds = fds;
printf("server waiting\n");
// If timeout is specified as NULL, select() blocks
// indefinitely waiting for a file descriptor to become
// ready.
// By syh: An incoming connection on sock_fd is also seen as a READABLE activity.
// Hence it's captured by `select`.
int res = select(FD_SETSIZE, &test_fds, NULL, NULL, NULL);
if (res < 1) {
printf("error!\n");
return -1;
}
for (int i = 0; i < FD_SETSIZE; i++) {
if (!FD_ISSET(i, &test_fds)) continue;
if (i == sock_fd) {
sockaddr client_addr;
socklen_t client_addr_len;
int client_fd = accept(sock_fd, &client_addr, &client_addr_len);
FD_SET(client_fd, &fds);
printf("adding client %d to fds\n", client_fd);
} else {
int nread;
ioctl(i, FIONREAD, &nread);
if (!nread) {
printf("removing connection %d\n", i);
FD_CLR(i, &fds);
close(i);
} else {
char buf[128];
read(i, &buf, nread); buf[nread] = 0;
printf("received from connection %d: %s\n", i, buf);
char out[] = "haha";
write(i, out, sizeof(out));
}
}
}
}
server v5: epoll
epoll与select不同,先注册一些events。当任意event被触发时,epoll_wait()返回,并填充预先设置的event数组。之后只要遍历这些event就可以了。
比select明显好的地方在于不需要遍历整个fd_set了。另外看上去和select也没啥区别。
void register_event(int epfd, int ops, int target_fd) {
struct epoll_event event;
event.events = ops;
event.data.fd = target_fd; // 别忘了这句!!!
epoll_ctl(epfd, EPOLL_CTL_ADD, target_fd, &event);
}
void unregister_event(int epfd, int ops, int target_fd) {
struct epoll_event event;
event.events = ops;
epoll_ctl(epfd, EPOLL_CTL_DEL, target_fd, &event);
}
struct epoll_event ep_events[20];
int main() {
sockaddr_in addr;
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
bind(sock_fd, (const sockaddr*)&addr, sizeof(addr));
listen(sock_fd, 5); // listen does NOT block
int epfd = epoll_create(10);
register_event(epfd, EPOLLIN, sock_fd);
while (true) {
printf("server waiting\n");
int num_events = epoll_wait(epfd, ep_events, 5, -1); // timeout = -1,即block
//printf("??");
if (num_events < 0) {
printf("error!\n");
return -1;
}
for (int i = 0; i < num_events; i++) {
int fd = ep_events[i].data.fd;
if (fd == sock_fd) {
sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(sock_fd, (sockaddr*)&client_addr, &client_addr_len);
register_event(epfd, EPOLLIN, client_fd);
printf("adding client %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
} else {
char buf[128];
int nread = read(fd, &buf, 128);
if (nread < 0) {
perror("read error!\n");
close (fd);
return -1;
}
else if (!nread) {
printf("removing connection %d\n", i);
unregister_event(epfd, EPOLLIN, fd);
close(fd);
} else {
buf[nread] = 0;
printf("received from connection %d: %s\n", fd, buf);
char out[] = "haha";
write(fd, out, sizeof(out));
}
}
}
}
}