早上看到一个读者说面字节三面的时候,问了这个问题:
这位读者的角度是以为服务端没有调用 listen,客户端会 ping 不通服务器,很明显,搞错了。
ping 使用的协议是 ICMP,属于网络层的事情,而面试官问的是传输层的问题。
针对这个问题,服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了 TCP 连接建立,此时那么会发生什么呢?
# 做个实验
这个问题,自己做个实验就知道了。
我用下面这个程序作为例子,绑定了 IP 地址 + 端口,而没有调用 listen。
/*******服务器程序 TCPServer.c ************/
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
int sockfd, ret;
struct sockaddr_in server_addr;
/* 服务器端创建 tcp socket 描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
fprintf(stderr, "Socket error:%s\n\a", strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr 结构 */
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
/* 绑定 ip + 端口 */
ret = bind(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));
if(ret < 0)
{
fprintf(stderr, "Bind error:%s\n\a", strerror(errno));
exit(1);
}
//没有调用 listen
sleep(1000);
close(sockfd);
return 0;
}
然后,我用浏览器访问这个地址:http://121.43.173.240:8888/
报错连接服务器失败。
同时,我也用抓包工具,抓了这个过程。
可以看到,客户端对服务端发起 SYN 报文后,服务端回了 RST 报文。
所以,这个问题就有了答案, 服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会回 RST 报文。
# 源码分析
接下来,带大家源码分析一下。
Linux 内核处理收到 TCP 报文的入口函数是 tcp_v4_rcv,在收到 TCP 报文后,会调用 __inet_lookup_skb 函数找到 TCP 报文所属 socket 。
int tcp_v4_rcv(struct sk_buff *skb)
{
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
...
}
__inet_lookup_skb 函数首先查找连接建立状态的socket(__inet_lookup_established),在没有命中的情况下,才会查找监听套接口(__inet_lookup_listener)。
查找监听套接口(__inet_lookup_listener)这个函数的实现是,根据目的地址和目的端口算出一个哈希值,然后在哈希表找到对应监听该端口的 socket。
本次的案例中,服务端是没有调用 listen 函数的,所以自然也是找不到监听该端口的 socket。
所以,__inet_lookup_skb 函数最终找不到对应的 socket,于是跳转到no_tcp_socket。
在这个错误处理中,只要收到的报文(skb)的「校验和」没问题的话,内核就会调用 tcp_v4_send_reset 发送 RST 中止这个连接。
至此,整个源码流程就解析完。
其实很多网络的问题,大家都可以自己做实验来找到答案的。
# 没有 listen,能建立 TCP 连接吗?
标题的问题在前面已经解答, 现在我们看另外一个相似的问题 。
之前看群消息,看到有读者面试腾讯的时候,被问到这么一个问题。
不使用 listen ,可以建立 TCP 连接吗?
答案, 是可以的,客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有listen,就能建立连接 。
那没有listen,为什么还能建立连接?
我们知道执行 listen 方法时,会创建半连接队列和全连接队列。
三次握手的过程中会在这两个队列中暂存连接信息。
所以形成连接,前提是你得有个地方存放着,方便握手的时候能根据 IP + 端口等信息找到对应的 socket。
那么客户端会有半连接队列吗?
显然没有,因为客户端没有执行listen,因为半连接队列和全连接队列都是在执行 listen 方法时,内核自动创建的。
但内核还有个全局 hash 表,可以用于存放 sock 连接的信息。
这个全局 hash 表其实还细分为 ehash,bhash和listen_hash等,但因为过于细节,大家理解成有一个全局 hash 就够了,
在 TCP 自连接的情况中,客户端在 connect 方法时,最后会将自己的连接信息放入到这个全局 hash 表中,然后将信息发出,消息在经过回环地址重新回到 TCP 传输层的时候,就会根据 IP + 端口信息,再一次从这个全局 hash 中取出信息。于是握手包一来一回,最后成功建立连接 。
TCP 同时打开的情况也类似,只不过从一个客户端变成了两个客户端而已。
做个实验
客户端自连接的代码,TCP socket 可以 connect 它本身 bind 的地址和端口:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define LOCAL_IP_ADDR (0x7F000001) // IP 127.0.0.1
#define LOCAL_TCP_PORT (34567) // 端口
int main(void)
{
struct sockaddr_in local, peer;
int ret;
char buf[128];
int sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&local, 0, sizeof(local));
memset(&peer, 0, sizeof(peer));
local.sin_family = AF_INET;
local.sin_port = htons(LOCAL_TCP_PORT);
local.sin_addr.s_addr = htonl(LOCAL_IP_ADDR);
peer = local;
int flag = 1;
ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
if (ret == -1) {
printf("Fail to setsocket SO_REUSEADDR: %s\n", strerror(errno));
exit(1);
}
ret = bind(sock, (const struct sockaddr *)&local, sizeof(local));
if (ret) {
printf("Fail to bind: %s\n", strerror(errno));
exit(1);
}
ret = connect(sock, (const struct sockaddr *)&peer, sizeof(peer));
if (ret) {
printf("Fail to connect myself: %s\n", strerror(errno));
exit(1);
}
printf("Connect to myself successfully\n");
//发送数据
strcpy(buf, "Hello, myself~");
send(sock, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
//接收数据
recv(sock, buf, sizeof(buf), 0);
printf("Recv the msg: %s\n", buf);
sleep(1000);
close(sock);
return 0;
}
编译运行:
通过 netstat 命令命令客户端自连接的 TCP 连接:
从截图中,可以看到 TCP socket 成功的“连接”了自己,并发送和接收了数据包,netstat 的输出更证明了 TCP 的两端地址和端口是完全相同的。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。