五、listener fd的初始化

本文详细解析了TCP监听器的创建过程及配置选项,包括socket的创建、非阻塞设置、复用地址等关键步骤,并阐述了如何将监听描述符插入到事件处理表中。

Listener fd的创建以及监听代码位于[src/proto_tcp.c]的tcp_bind_listener函数中。

[src/proto_tcp.c] tcp_bind_listener()
int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen)
{
	__label__ tcp_return, tcp_close_return;
	int fd, err;
	const char *msg = NULL;

	/* ensure we never return garbage */
	if (errmsg && errlen)
		*errmsg = 0;

	if (listener->state != LI_ASSIGNED)
		return ERR_NONE; /* already bound */

为什么对于listener->state != LI_ASSIGNED的情况下,它就是绑定的了呢?那么看下listener入队时候是怎么操作的。

[src/proto_tcp.c] tcpv4_add_listener()
/* Add listener to the list of tcpv4 listeners. The listener's state
 * is automatically updated from LI_INIT to LI_ASSIGNED. The number of
 * listeners is updated. This is the function to use to add a new listener.
 */
void tcpv4_add_listener(struct listener *listener)
{
	if (listener->state != LI_INIT)
		return;
	listener->state = LI_ASSIGNED;
	listener->proto = &proto_tcpv4;
	LIST_ADDQ(&proto_tcpv4.listeners, &listener->proto_list);
	proto_tcpv4.nb_listeners++;
}

很明显,listener在入队时状态会自动改变成LI_ASSIGNED,因此对于处于队列中的listener,他们的状态至少为LI_ASSIGNED,也就是说,如果不为ASSIGNED,那么必定已经绑定过来了。对于已经绑定然后解绑定的listener,只要没有移出队列,那么其状态也是LI_ASSIGNED。如果从队列中移除,那么状态就要小于LI_ASSIGNED了,从添加的过程要求状态为LI_INIT,大致可以知道,其从队列删除时,状态会变成LI_INIT,事实上也是如此。

[src/proto_tcp.c] tcp_bind_listener()
	err = ERR_NONE;

	if ((fd = socket(listener->addr.ss_family, SOCK_STREAM, IPPROTO_TCP)) == -1) {
		err |= ERR_RETRYABLE | ERR_ALERT;
		msg = "cannot create listening socket";
		goto tcp_return;
	}

	if (fd >= global.maxsock) {
		err |= ERR_FATAL | ERR_ABORT | ERR_ALERT;
		msg = "not enough free sockets (raise '-n' parameter)";
		goto tcp_close_return;
	}

	if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
		err |= ERR_FATAL | ERR_ALERT;
		msg = "cannot make socket non-blocking";
		goto tcp_close_return;
	}

创建socket,然后判断数量是否超越最大值(作者很多地方都习惯于先分配再判断,这应该是与其维护内核代码相关的)。如果没有超出限制,那么将fd设置为非阻塞。

[src/proto_tcp.c] tcp_bind_listener()
	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &one, sizeof(one)) == -1) {
		/* not fatal but should be reported */
		msg = "cannot do so_reuseaddr";
		err |= ERR_ALERT;
	}

	if (listener->options & LI_O_NOLINGER)
		setsockopt(fd, SOL_SOCKET, SO_LINGER, (struct linger *) &nolinger, sizeof(struct linger));

#ifdef SO_REUSEPORT
	/* OpenBSD supports this. As it's present in old libc versions of Linux,
	 * it might return an error that we will silently ignore.
	 */
	setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (char *) &one, sizeof(one));
#endif
#ifdef CONFIG_HAP_LINUX_TPROXY
	if ((listener->options & LI_O_FOREIGN)
	    && (setsockopt(fd, SOL_IP, IP_TRANSPARENT, (char *) &one, sizeof(one)) == -1)
	    && (setsockopt(fd, SOL_IP, IP_FREEBIND, (char *) &one, sizeof(one)) == -1)) {
		msg = "cannot make listening socket transparent";
		err |= ERR_ALERT;
	}
#endif
#ifdef SO_BINDTODEVICE
	/* Note: this might fail if not CAP_NET_RAW */
	if (listener->interface) {
		if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,
			       listener->interface, strlen(listener->interface) + 1) == -1) {
			msg = "cannot bind listener to device";
			err |= ERR_WARN;
		}
	}
#endif
#if defined(TCP_MAXSEG)
	if (listener->maxseg) {
		if (setsockopt(fd, IPPROTO_TCP, TCP_MAXSEG,
			       &listener->maxseg, sizeof(listener->maxseg)) == -1) {
			msg = "cannot set MSS";
			err |= ERR_WARN;
		}
	}
#endif
#if defined(TCP_DEFER_ACCEPT)
	if (listener->options & LI_O_DEF_ACCEPT) {
		/* defer accept by up to one second */
		int accept_delay = 1;
		if (setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &accept_delay, sizeof(accept_delay)) == -1) {
			msg = "cannot enable DEFER_ACCEPT";
			err |= ERR_WARN;
		}
	}
#endif

根据配置,设置socket的选项。其中要说明的是TCP_DEFER_ACCEPT,这个选项要求在backlog中的连接,需要有数据才会被应用程序接收。

[src/proto_tcp.c] tcp_bind_listener()
	if (bind(fd, (struct sockaddr *)&listener->addr, listener->proto->sock_addrlen) == -1) {
		err |= ERR_RETRYABLE | ERR_ALERT;
		msg = "cannot bind socket";
		goto tcp_close_return;
	}

	if (listen(fd, listener->backlog ? listener->backlog : listener->maxconn) == -1) {
		err |= ERR_RETRYABLE | ERR_ALERT;
		msg = "cannot listen to socket";
		goto tcp_close_return;
	}

#if defined(TCP_QUICKACK)
	if (listener->options & LI_O_NOQUICKACK)
		setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (char *) &zero, sizeof(zero));
#endif

	/* the socket is ready */
	listener->fd = fd;
	listener->state = LI_LISTEN;

绑定并监听,而且根据配置可选地关闭掉TCP_QUICKACK。此时listener的状态也要相应的调整为LI_LISTEN。

[src/proto_tcp.c] tcp_bind_listener()
	/* the function for the accept() event */
	fd_insert(fd);
	fdtab[fd].cb[DIR_RD].f = listener->accept;
	fdtab[fd].cb[DIR_WR].f = NULL; /* never called */
	fdtab[fd].cb[DIR_RD].b = fdtab[fd].cb[DIR_WR].b = NULL;
	fdtab[fd].owner = listener; /* reference the listener instead of a task */
	fdtab[fd].state = FD_STLISTEN;
	fdtab[fd].flags = FD_FL_TCP;
	if (listener->options & LI_O_NOLINGER)
		fdtab[fd].flags |= FD_FL_TCP_NOLING;

	fdinfo[fd].peeraddr = NULL;
	fdinfo[fd].peerlen = 0;

将fd插入fdtab中。并设置一些状态,以及回调函数。从此处可以看出,监听描述符对应的read函数来自于listeneraccept成员,write函数为空,对于fdtab中的listner fd来说,他们的owner为对应的listnerListneraccept成员被赋予event_accept函数,这是在解析配置文件的过程中进行的。待以后讲解main函数的时候再讲解。对于listener要求设置NOLINGER选项的,还需要加上程序本身的NOLINGER选项。

[proto/fd.h] fd_insert()
static inline void fd_insert(int fd)
{
	fdtab[fd].ev = 0;
	if (fd + 1 > maxfd)
		maxfd = fd + 1;
}

Fd_insert的操作非常简单。将从poll中返回已处理事件置空,然后更新maxfd

[src/proto_tcp.c] tcp_bind_listener()
 tcp_return:
	if (msg && errlen)
		strlcpy2(errmsg, msg, errlen);
	return err;

 tcp_close_return:
	close(fd);
	goto tcp_return;
}

此处Listener fd的操作与常见的操作不一样的只有最后对于fdtab的处理。

<think>我们正在讨论uhttpd中的两个全局链表:uh_listeners和uh_clients。这两个链表分别用于管理监听套接字和客户端连接。 1. `static struct listener *uh_listeners = NULL;` - 这是一个链表头,指向所有的监听器(listener)。每个监听器对应一个监听的套接字(socket),用于接受客户端的连接。 - 在`uh_listener_add`函数中,当创建一个新的监听器时,会将其加入到这个链表的头部(通过`new->next = uh_listeners;`然后`uh_listeners = new;`)。 2. `static struct client *uh_clients = NULL;` - 这是一个链表头,指向所有已连接的客户端(client)。每个客户端结构代表一个已经建立的客户端连接(即accept之后得到的套接字)。 - 每当通过监听套接字accept一个新的客户端连接时,就会创建一个`struct client`对象,并加入到`uh_clients`链表中。 在`uh_listener_add`函数中: - 参数:`int sock` 是要加入监听的套接字描述符,`struct config *conf` 是与该监听器相关的配置。 - 函数功能:创建一个新的监听器结构体,初始化它,并将其添加到`uh_listeners`链表的头部。 - 步骤: a. 分配内存并清零。 b. 设置套接字描述符和配置。 c. 使用`getsockname`获取该套接字绑定的本地地址(包括IP和端口),并保存在`new->addr`中。 d. 将新监听器插入链表头部。 所以,`uh_listeners`链表中的每个节点代表一个服务器正在监听的端口(比如8085端口)。当有客户端连接请求到达时,服务器会通过监听套接字接受连接,并为该连接创建一个客户端结构(`struct client`),然后将其加入`uh_clients`链表。 而`uh_clients`链表则用于管理当前所有活跃的客户端连接。在事件循环中(例如使用epoll或select),程序会监控这些客户端套接字上的读写事件,并进行相应的处理。 总结: - `struct listener`:表示一个监听端点(IP地址和端口),用于接受新的连接。 - `struct client`:表示一个已经建立的客户端连接,用于与客户端进行数据交换。 因此,在客户端(浏览器或curl)发起连接时,首先会通过`uh_listeners`中的某个监听套接字接受连接,然后创建一个`struct client`对象并加入`uh_clients`链表,后续的数据传输都通过这个客户端结构进行。 注意:在uhttpd中,通常只有一个监听器(监听80或8085等),但如果有多个配置(比如监听多个端口),那么就会有多个监听器。 §§相关问题§§ 1. 在uhttpd中,如何管理多个监听器(listener)上的事件? 2. 当一个新的客户端连接到达时,uhttpd是如何将其加入到uh_clients链表的? 3. 在客户端断开连接后,uh_clients链表中的节点是如何被移除的? 4. 为什么在uh_listener_add函数中要将新的监听器插入到链表头部而不是尾部? 5. 在多线程环境下,对uh_listeners和uh_clients链表的操作是否需要加锁保护? </think>### uHTTPd 中的 `listener` 和 `client` 结构说明 #### 1. `struct listener`(监听器) ```c static struct listener *uh_listeners = NULL; // 全局监听器链表 struct listener * uh_listener_add(int sock, struct config *conf) { // ... new->fd.fd = sock; // 套接字描述符 new->conf = conf; // 服务器配置 getsockname(sock, ...); // 获取绑定的地址端口 new->next = uh_listeners; // 插入链表头部 uh_listeners = new; // 更新全局指针 } ``` - **功能**:代表一个网络监听端点 - **客户端视角**:服务器的 IP:端口(如 `192.168.0.1:8085`) - **关键成员**: - `fd.fd`:监听套接字(通过`socket()`+`bind()`+`listen()`创建) - `conf`:服务器配置(端口/TLS/认证等) - `addr`:绑定的网络地址(通过`getsockname()`获取) - **生命周期**:服务器启动时创建,关闭时销毁 #### 2. `struct client`(客户端连接) ```c static struct client *uh_clients = NULL; // 全局客户端链表 // 伪代码:客户端创建逻辑 void accept_new_client(int listen_fd) { int client_fd = accept(listen_fd, ...); struct client *cl = malloc(sizeof(struct client)); cl->fd = client_fd; // 客户端套接字 cl->peeraddr = client_addr; // 客户端地址 cl->next = uh_clients; // 插入链表头部 uh_clients = cl; // 更新全局指针 } ``` - **功能**:代表一个活跃的客户端连接 - **客户端视角**:浏览器/curl 建立的 TCP 连接 - **关键成员**: - `fd`:客户端套接字(通过`accept()`创建) - `peeraddr`:客户端地址(通过`getpeername()`获取) - `request_body`:HTTP 请求体数据 - **生命周期**:客户端连接时创建,断开时销毁 --- ### 工作流程对比 | **阶段** | `listener` 作用 | `client` 作用 | |----------------|------------------------------------------|----------------------------------------| | **启动服务** | 创建监听套接字,注册到全局链表 | - | | **接收连接** | 通过`select()`/`epoll()`等待连接 | 当`accept()`成功后创建新client对象 | | **处理请求** | 不直接参与 | 解析HTTP请求,执行业务逻辑 | | **发送响应** | 不直接参与 | 通过`write()`返回数据给客户端 | | **关闭连接** | 保持监听 | 从链表移除,关闭套接字,释放内存 | | **停止服务** | 关闭所有监听套接字,释放链表 | 强制关闭所有客户端连接 | --- ### 关键数据结构关系 ```mermaid graph TD A[uh_listeners] --> B[listener1] B --> |fd=8085| C[监听套接字] A --> D[listener2] D --> |fd=443| E[HTTPS套接字] F[uh_clients] --> G[client1] G --> |fd=1001| H[curl连接] F --> I[client2] I --> |fd=1002| J[浏览器连接] C -->|accept| H E -->|accept| J ``` --- ### 实际应用场景 1. **多端口监听**: ```c // 同时监听HTTP和HTTPS uh_listener_add(create_socket(8085), conf_http); uh_listener_add(create_socket(443), conf_https); ``` 2. **客户端管理**: ```c // 遍历所有客户端发送广播 struct client *cl = uh_clients; while (cl) { uh_http_sendf(cl, "HTTP/1.1 200 OK\r\n..."); cl = cl->next; } ``` 3. **连接超时处理**: ```c // 定时检查闲置客户端 if (now - cl->last_activity > TIMEOUT) { uh_client_close(cl); // 关闭超时连接 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值