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 <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);}//没有调用 listensleep(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 <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 的两端地址和端口是完全相同的。

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(),客户端前面发送的消息,还是能正常收到的。

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

图解网络(三)——TCP篇07相关推荐

  1. 【操作系统三】图解网络IO(bio\nio\slect\epoll)

    [操作系统三]图解网络IO+实战 一.计算机组成 二.系统中断 三.晶振(时间中断.分时复用) 四.事件中断 1.DMA 2.事件中断 3.网卡也会产生中断? 五.linux系统知识 1.linux下 ...

  2. Visual C#.Net网络程序开发-Tcp篇(1)

    Visual C#.Net 网络程序开发-Socket篇 Visual C#.Net网络程序开发-Tcp篇(1) Visual C#.Net网络程序开发-Tcp篇(2) Visual C#.Net网络 ...

  3. 黑马程序员_java高级篇网络编程TCP实战Day8(上)

    ---------------------ASP.Net+Android+IOS开发.Net培训.期待与您交流! ----------- 黑马程序员_java高级篇网络编程TCP实战Day8(上) ( ...

  4. 图解网络TCP/IP

    图解网络TCP/IP 通信模型参考 TCP 三次握手和四次挥手 常见网络协议 参考 通信模型参考 OSI 模型注重通信协议必要的功能:TCP/IP更强调在计算机上实现协议应用开发哪种程序 应用层:提供 ...

  5. python socket 主动断开_Python网络编程tcp详解(基础篇十四)

    网络编程tcp 1 TCP详解 <1> tcp概述 TCP:英文全拼(Transmission Control Protocol)简称传输控制协议,它是一种面向连接的.可靠的.基于字节流的 ...

  6. 万物互联之~网络编程基础篇

    入门篇¶ 官方文档:https://docs.python.org/3/library/ipc.html(进程间通信和网络) 实例代码:https://github.com/lotapp/BaseCo ...

  7. Silverlight+WCF 实战-网络象棋最终篇之对战视频-下篇[客户端发送与服务端中转](六)...

    本篇继上一篇:Silverlight+WCF 实战-网络象棋最终篇之对战视频-上篇[客户端开启视频/注册编号/接收视频](五)  一:对战视频 简单原理 略,内容见上篇. 二:对战视频 步骤解析: 略 ...

  8. Silverlight+WCF 实战-网络象棋最终篇之对战视频-上篇[客户端开启视频/注册编号/接收视频](五)...

    前言: 近期在忙点"秋色园"的事情,所以网络象棋这一块文章就写的相对慢,而且刚好接上篇:Silverlight+WCF 实战-网络象棋最终篇之非线程阻塞倒计时窗口(四)  之后, ...

  9. 网络编程-TCP/IP协议栈-TCP协议

    TCP协议 TCP协议作用 TCP协议位于协议栈的传输层.当应用层向TCP层发送用于网间传输的,用8字节表示的数据流,TCP则吧数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受到改计算机连 ...

最新文章

  1. Java之String、StringBuffer、StringBulider辨识
  2. 高级C语言教程-关键字和运算符
  3. linux中timer的作用,linux - linux / timer.h setup_timer()到期功能不起作用? - 堆栈内存溢出...
  4. phpcms文件所需权限
  5. transition:background-color .3s 背景色 - 渐变效果
  6. go语言代码规范详解
  7. 合工大计算机与信息学院保研,合肥工业大学计算机与信息学院(专业学位)计算机技术保研细则...
  8. vue动态改变css样式
  9. 各种数据结构优缺点分析
  10. Win11用户好消息 影响win11性能运行的竟是它,关闭可提升性能
  11. html多个div横向排列居中,多个div垂直居中横向排列的示例分析
  12. 宏晶微MS2109高清视频采集芯片资料
  13. 10只狗怎么鉴别1000瓶水哪瓶有毒
  14. 入门 HTML JavaScript Jquery学习回顾 有小案例
  15. (P45)面向对象版表达式计算器:Storage类实现
  16. oracle索引管理
  17. 关于自制CMSIS_DAP离线下载器下载算法的代码说明:“0xE00ABE00, 0x062D780D, 0x24084068, 0xD3000040, 0x1E644058, 0x1C49D1FA“
  18. 试卷模板 html,A4纸试卷模板.doc
  19. 展锐Android11安兔兔显示的光圈值修正
  20. vue手机号中间四位加*号

热门文章

  1. Edge浏览器的书签(收藏夹)文件夹地址在哪?
  2. 毕业论文致谢到底要怎么写?
  3. Java之png图片工具类
  4. transform:scale实现大屏自适应
  5. 通过网线连接获取树莓派的ip地址
  6. 【新年返程离不开Python】最新12306抢票源程序Python版就此分享给大家啦!
  7. 在线硬盘存储计算机,硬盘存储
  8. 不可不知!4种常见的黑客攻击手段
  9. python的类中 _、__和__xx__的区别
  10. Linux下打开ISO文件两种方法