3 TCP篇

3.19 TCP和UDP可以使用同一个端口么

关于端口的知识点,还是挺多可以讲的,比如还可以牵扯到这几个问题:

多个 TCP 服务进程可以同时绑定同一个端口吗?重启 TCP 服务进程时,为什么会出现“Address in use”的报错信息?又该怎么避免?客户端的端口可以重复使用吗?客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?

所以,这次就跟大家盘一盘这些问题。

3.19.1 TCP和UDP可以绑定相同的端口吗

其实我感觉这个问题「TCP 和 UDP 可以同时监听相同的端口吗?」表述有问题,这个问题应该表述成「TCP 和 UDP 可以同时绑定相同的端口吗?」

因为「监听」这个动作是在 TCP 服务端网络编程中才具有的,而 UDP 服务端网络编程中是没有「监听」这个动作的。

TCP 和 UDP 服务端网络相似的一个地方,就是会调用 bind 绑定端口。

给大家贴一下 TCP 和 UDP 网络编程的区别就知道了。

TCP 网络编程如下,服务端执行 listen() 系统调用就是监听端口的动作。

UDP 网络编程如下,服务端是没有监听这个动作的,只有执行 bind() 系统调用来绑定端口的动作。

TCP 和 UDP 可以同时绑定相同的端口吗?

答案:可以的。

在数据链路层中,通过 MAC 地址来寻找局域网中的主机。在网际层中,通过 IP 地址来寻找网络中互连的主机或路由器。在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。

所以,传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。

传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。

当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

因此, TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。

3.19.2 多个TCP服务进程可以绑定同一个端口吗

如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。

如果两个 TCP 服务进程绑定的 IP 地址不同,而端口相同的话,也是可以绑定成功的

3.19.3 客户端的端口可以被重复使用吗

客户端在执行 connect 函数的时候,会在内核里随机选择一个端口,然后向服务端发起 SYN 报文,然后与服务端进行三次握手。

所以,客户端的端口选择的发生在 connect 函数,内核在选择端口的时候,会从 net.ipv4.ip_local_port_range 这个内核参数指定的范围来选取一个端口作为客户端端口。

该参数的默认值是 32768 61000,意味着端口总可用的数量是 61000 - 32768 = 28232 个。

当客户端与服务端完成 TCP 连接建立后,我们可以通过 netstat 命令查看 TCP 连接。

$ netstat -napt

协议 源ip地址:端口 目的ip地址:端口 状态

tcp 192.168.110.182.64992 117.147.199.51.443 ESTABLISHED

那问题来了,上面客户端已经用了 64992 端口,那么还可以继续使用该端口发起连接吗?

这个问题,很多同学都会说不可以继续使用该端口了,如果按这个理解的话, 默认情况下客户端可以选择的端口是 28232 个,那么意味着客户端只能最多建立 28232 个 TCP 连接,如果真是这样的话,那么这个客户端并发连接也太少了吧,所以这是错误理解。

正确的理解是,TCP 连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的。所以如果客户端已使用端口 64992 与服务端 A 建立了连接,那么客户端要与服务端 B 建立连接,还是可以使用端口 64992 的,因为内核是通过四元祖信息来定位一个 TCP 连接的,并不会因为客户端的端口号相同,而导致连接冲突的问题。

3.20 服务器端没有listen,客户端发起连接建立,会发生什么

我用下面这个程序作为例子,绑定了 IP 地址 + 端口,而没有调用 listen。

/*******服务器程序 TCPServer.c ************/

#include

#include

#include

#include

#include

#include

#include

#include

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 报文。

不使用 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

#include

#include

#include

#include

#include

#include

#include

#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 的两端地址和端口是完全相同的。

3.21 没有accept,能建立TCP连接么

下面一段简化过的服务端伪代码。

int main()

{

/*Step 1: 创建服务器端监听socket描述符listen_fd*/

listen_fd = socket(AF_INET, SOCK_STREAM, 0);

/*Step 2: bind绑定服务器端的IP和端口,所有客户端都向这个IP和端口发送和请求数据*/

bind(listen_fd, xxx);

/*Step 3: 服务端开启监听*/

listen(listen_fd, 128);

/*Step 4: 服务器等待客户端的链接,返回值cfd为客户端的socket描述符*/

cfd = accept(listen_fd, xxx);

/*Step 5: 读取客户端发来的数据*/

n = read(cfd, buf, sizeof(buf));

}

估计大家也是老熟悉这段伪代码了。

需要注意的是,在执行listen()方法之后还会执行一个accept()方法。

一般情况下,如果启动服务器,会发现最后程序会阻塞在accept()里。

此时服务端就算ok了,就等客户端了。

那么,再看下简化过的客户端伪代码。

int main()

{

/*Step 1: 创建客户端端socket描述符cfd*/

cfd = socket(AF_INET, SOCK_STREAM, 0);

/*Step 2: connect方法,对服务器端的IP和端口号发起连接*/

ret = connect(cfd, xxxx);

/*Step 4: 向服务器端写数据*/

write(cfd, buf, strlen(buf));

}

客户端比较简单,创建好socket之后,直接就发起connect方法。

此时回到服务端,会发现之前一直阻塞的accept方法,返回结果了。

这就算两端成功建立好了一条连接。之后就可以愉快的进行读写操作了。

那么,我们今天的问题是,如果没有这个accept方法,TCP连接还能建立起来吗?

其实只要在执行accept() 之前执行一个 sleep(20),然后立刻执行客户端相关的方法,同时抓个包,就能得出结论。

从抓包结果看来,就算不执行accept()方法,三次握手照常进行,并顺利建立连接。

更骚气的是,在服务端执行accept()前,如果客户端发送消息给服务端,服务端是能够正常回复ack确认包的。

并且,sleep(20)结束后,服务端正常执行accept(),客户端前面发送的消息,还是能正常收到的。

通过这个现象,我们可以多想想为什么。顺便好好了解下三次握手的细节。

3.22 用了TCP协议,数据一定不会丢么

我们知道TCP位于传输层,在它的上面还有各种应用层协议,比如常见的HTTP或者各类RPC协议。

TCP保证的可靠性,是传输层的可靠性。也就是说,TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。

至于数据到了接收端的传输层之后,能不能保证到应用层,TCP并不管。

假设现在,我们输入一条消息,从聊天框发出,走到传输层TCP协议的发送缓冲区,不管中间有没有丢包,最后通过重传都保证发到了对方的传输层TCP接收缓冲区,此时接收端回复了一个ack,发送端收到这个ack后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。

TCP任务是结束了,但聊天软件的任务没结束。

聊天软件还需要将数据从TCP的接收缓冲区里读出来,如果在读出来这一刻,手机由于内存不足或其他各种原因,导致软件崩溃闪退了。

发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。

于是乎,消息就丢了。

/*Step 5: 读取客户端发来的数据*/

n = read(cfd, buf, sizeof(buf));

}

估计大家也是老熟悉这段伪代码了。

需要注意的是,在执行`listen()`方法之后还会执行一个`accept()`方法。

**一般情况**下,如果启动服务器,会发现最后程序会**阻塞在**`accept()`里。

此时服务端就算ok了,就等客户端了。

那么,再看下简化过的客户端伪代码。

```c

int main()

{

/*Step 1: 创建客户端端socket描述符cfd*/

cfd = socket(AF_INET, SOCK_STREAM, 0);

/*Step 2: connect方法,对服务器端的IP和端口号发起连接*/

ret = connect(cfd, xxxx);

/*Step 4: 向服务器端写数据*/

write(cfd, buf, strlen(buf));

}

客户端比较简单,创建好socket之后,直接就发起connect方法。

此时回到服务端,会发现之前一直阻塞的accept方法,返回结果了。

这就算两端成功建立好了一条连接。之后就可以愉快的进行读写操作了。

那么,我们今天的问题是,如果没有这个accept方法,TCP连接还能建立起来吗?

其实只要在执行accept() 之前执行一个 sleep(20),然后立刻执行客户端相关的方法,同时抓个包,就能得出结论。

[外链图片转存中…(img-3ZMi4sku-1661313437050)]

从抓包结果看来,就算不执行accept()方法,三次握手照常进行,并顺利建立连接。

更骚气的是,在服务端执行accept()前,如果客户端发送消息给服务端,服务端是能够正常回复ack确认包的。

并且,sleep(20)结束后,服务端正常执行accept(),客户端前面发送的消息,还是能正常收到的。

通过这个现象,我们可以多想想为什么。顺便好好了解下三次握手的细节。

好文链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。