Java网络编程,使用UDP实现TCP(一), 基本实现三次握手

简介:

首先我们需要知道TCP传输和UDP传输的区别,UDP相当于只管发送不管对方是否接收到了,而TCP相当于打电话,需要进行3次握手,4次挥手,所以我们就需要在应用层上做一些功能添加,如:

  • 增加ack机制

  • 增加seq机制

  • 增加超时重传机制

  • 增加MTU机制

  • 增加数据校验机制

即可实现简单的用UDP实现TCP功能。

part1:了解Java网络编程如何实现UDP和TCP

UDP:

UDP客户端发送数据:

  • 创建UDP套接字:使用DatagramSocket类创建一个UDP套接字,用于发送和接收UDP数据报。可以指定端口号或让系统自动分配一个可用端口。

  • 准备发送的数据,转成字节数组。

  • 构造UDP数据报:创建一个DatagramPacket对象,用于封装要发送的数据和目标主机的信息。需要提供要发送的数据的字节数组、数据的长度、目标主机的IP地址和端口号。

  • 发送数据报:使用UDP套接字的send()方法将封装好的数据报发送给目标主机。该方法接受一个DatagramPacket对象作为参数。

  • 关闭套接字:使用UDP套接字的close()方法关闭套接字,释放相关的资源。

import java.io.IOException;
import java.net.*;public class UDPClient {public static void main(String[] args) throws IOException {System.out.println("发送启动中。。。");//1. 使用 DatagramSocket(8888)DatagramSocket datagramSocket = new DatagramSocket(8888);//2. 准备数据,一定要转成字节数组String data = "hello java";//创建数据,并把数据打包byte[] datas = "hello java".getBytes();DatagramPacket datagramPacket = new DatagramPacket(datas, 0,datas.length, new InetSocketAddress("localhost",9999));//调用对象发送数据datagramSocket.send(datagramPacket);//关闭流datagramSocket.close();}
}

 UDP服务端接收数据:

  • 创建UDP套接字:使用DatagramSocket类创建一个UDP套接字,用于发送和接收UDP数据报。可以指定端口号或让系统自动分配一个可用端口。

  • 创建一个字节数组用于接收发送的数据。

  • 构造UDP数据报:创建一个DatagramPacket对象,用于封装要发送的数据和目标主机的信息。需要提供要发送的数据的字节数组、数据的长度、目标主机的IP地址和端口号。

  • 发送数据报:使用UDP套接字的receive()方法将封装好的数据报发送给目标主机。该方法接受一个DatagramPacket对象作为参数。

  • 关闭套接字:使用UDP套接字的close()方法关闭套接字,释放相关的资源。

package TCP_UDP_Practice.UDPrecieve;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;public class UDPClient {public static void main(String[] args) throws IOException {System.out.println("接收方接收中。。。");DatagramSocket datagramSocket = new DatagramSocket(9999);byte[] container = new byte[1024 * 60];DatagramPacket packet = new DatagramPacket(container, 0, container.length);datagramSocket.receive(packet);System.out.println(new String(packet.getData(), 0, packet.getLength()));datagramSocket.close();}
}

TCP:

TCP客户端发送数据:

  • 创建TCP客户端套接字:在服务器接受到客户端的连接请求后,将创建一个新的TCP套接字,用于和客户端进行通信。服务器套接字和客户端套接字之间建立了一条连接。

  • 数据传输:使用客户端套接字和服务器套接字进行数据传输。可以使用输出流来写入数据。服务器套接字和客户端套接字之间可以进行双向的数据传输。

  • 关闭连接:当数据传输完成或需要关闭连接时,可以调用套接字的close()方法关闭连接。关闭连接后,服务器套接字将继续监听新的连接请求。

package TCP_UDP_Practice.TCPsendMsg;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;public class ClientDemo {public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1", 10005);//创建输入流对象,写入数据OutputStream outputStream = socket.getOutputStream();outputStream.write("hello tcp".getBytes());//关闭流socket.close();}
}

TCP服务端接收数据:

  • 创建TCP服务器套接字:使用ServerSocket类创建一个TCP服务器套接字,用于监听客户端的连接请求。需要指定服务器的端口号。
  • 数据传输:使用客户端套接字和服务器套接字进行数据传输。可以使用输入流来读取数据。服务器套接字和客户端套接字之间可以进行双向的数据传输。

  • 关闭连接:当数据传输完成或需要关闭连接时,可以调用套接字的close()方法关闭连接。关闭连接后,服务器套接字将继续监听新的连接请求。

package TCP_UDP_Practice.TCPrecieve;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;public class ServerDemo {public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(10005);Socket accept = serverSocket.accept();//获取输入流InputStream inputStream = accept.getInputStream();byte[] bytes = new byte[1024];int read = inputStream.read(bytes);String s = new String(bytes, 0, read);System.out.println("数据是:" + s);//关闭流serverSocket.close();}
}

Part2:用UDP如何实现TCP的三次握手?

参考《TCP/IP详解》卷一的424页,我们可以得知三次握手须传输的主要数据有SYN, Seq和ACK,接下来我将详细说说三次握手这些数据有何变化,如何获取。

第一次握手:

  • 客户端会发送一个SYN 报文段(即一个在TCP头部位置SYN位置的TCP/IP数据包),并指明自己想要连接到的端口号和它的客户端初始序列号ISN。客户端发送的这个SYN报文段称为段1。
  •  那么问题来了:SYN,ISN到底如何获取,如何用Java程序写出来呢?
    • SYN:(Synchronize)是TCP(传输控制协议)中的一个标志位,用于建立连接的过程中进行同步。在TCP三次握手的过程中,SYN用于表示发起连接请求的一方(通常是客户端)希望建立连接。SYN标志位的值为1,表示发起连接请求或确认连接请求。
    • Seq:(Sequence Number)是用于标识数据字节顺序的字段。每个TCP报文段都包含一个Seq字段,用于指示报文段中的数据字节在整个数据流中的位置。

      • Seq字段的值表示报文段中的第一个数据字节的序列号。每个字节都有一个唯一的序列号,序列号从一个初始值开始,并随着每个传输的字节递增。

      • 在TCP连接建立后,双方会通过ISN(Initial Sequence Number)来初始化序列号。ISN是一个随机选择的32位无符号整数,用作初始的序列号。之后,发送方在发送数据时,会为每个报文段分配一个递增的序列号。

      • 接收方在接收到报文段时,根据Seq字段的值来确定数据字节的顺序。如果接收方发现某个报文段的Seq值不连续或重复,它会通知发送方进行相应的处理,以确保数据的正确传输和重组。

      • Seq字段的作用是保证TCP数据的有序性和可靠性。通过正确的序列号,接收方可以按正确的顺序重组数据,并检测丢失或重复的数据。

      • 需要注意的是,Seq字段的范围是32位无符号整数,因此序列号会在达到最大值后重新从0开始循环。

    • ISN:(Initial Sequence Number)是TCP(传输控制协议)中用于初始化序列号的值。序列号用于标识TCP报文段中的数据字节顺序,以便接收方可以按正确的顺序重组数据。

      在TCP连接建立时,双方需要协商一个初始的序列号。

      • ISN是一个随机选择的32位无符号整数,通常由操作系统生成。ISN的选择是为了增加连接的安全性,防止恶意攻击者猜测序列号并插入伪造的数据。

      • ISN的选择是根据一些算法和系统状态进行的,具体的实现可能因操作系统而异。通常,ISN的选择会考虑到时间、IP地址、端口号等因素,以确保序列号的唯一性和随机性。在[RFC1948]中提出了一个较好的初始化序列号ISN随机生成算法。ISN = M + F(localhost, localport, remotehost, remoteport). 

        注意:M是一个计时器,这个计时器每隔4毫秒加1。F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证hash算法不能被外部轻易推算得出,用MD5算法是一个比较好的选择。
      • 一旦双方在三次握手过程中成功建立连接,ISN就会被用作初始的序列号,并在后续的数据传输中递增。序列号的递增是为了确保数据的有序传输和重组。

      • 需要注意的是,ISN是每个TCP连接独立选择的,不同的连接会有不同的ISN。这样可以避免一个连接中的序列号被用于另一个连接,从而增加连接的安全性。

ISN初始化代码如下:

package TCP_handShake;import java.time.LocalDateTime;
import java.util.UUID;/*** 初始化Seq的值ISN* RFC1948中提出了一个较好的初始化序列号ISN随机生成算法:* ISN = M + F(localhost, localport, remotehost, remoteport).**/
public class initializeISN {private int ISN = generateISN() ;public int getISN() {return ISN;}private int generateISN(){// 获取当前时间String currentTime = String.valueOf(LocalDateTime.now().getSecond());// 生成UUIDUUID uuid = UUID.randomUUID();// 将时间和UUID结合生成ISNString isnString = currentTime + uuid.toString();int isn = isnString.hashCode();return isn;}
}

在我的代码中,由于我的目的是简单的实现,所以并未采用 [RFC1948]提到的算法,而是使用当前时间的秒数(通过LocalDateTime类得到)和UUID进行字符串拼接,实现了唯一性。(由于没有做到后面的内容,如后续如发现有问题,会进行更改

SYN和Seq初始化代码如下

package TCP_handShake;/*** 标志位 connectionMarks*/
public class ConnectionMarks extends initializeISN{//每次建立新连接,将SYN初始化为1private int SYN;//获取ISNprivate int Seq;public ConnectionMarks() {this.SYN = 1;this.Seq = getISN();}public int getSeq() {return Seq;}//setter of SYNpublic Integer getSYN() {return SYN;}
}

第一次握手客户端发送数据:

 System.out.println("第一次握手:");System.out.println("正在发送SYN和Seq......");//1. 使用 DatagramSocket(8888)DatagramSocket datagramSocket = new DatagramSocket(8888);ConnectionMarks connectionMarks = new ConnectionMarks();String SYN = String.valueOf(connectionMarks.getSYN());//getSeq() 方法值等同于 getISN(),获取ISN(c)int ISN1 = connectionMarks.getSeq();String Seq = String.valueOf(ISN1);//2. 准备数据,一定要转成字节数组String data = SYN + " " + Seq;//创建数据,并把数据打包byte[] datas = data.getBytes();DatagramPacket datagramPacket = new DatagramPacket(datas, 0,datas.length, new InetSocketAddress("localhost",9999));//调用对象发送数据datagramSocket.send(datagramPacket);//关闭流datagramSocket.close();

第一次握手服务端接收数据:

  System.out.println("接收数据:...");//创建接收端对象DatagramSocket datagramSocket = new DatagramSocket(9999);//创建数据包,用于接收数据byte[] bytes = new byte[1024];DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);datagramSocket.receive(datagramPacket);String s = new String(datagramPacket.getData(), 0, datagramPacket.getLength());//解析数据包并且输出显示System.out.println("数据为: " + s);//关闭流datagramSocket.close();

第二次握手:

  1. 服务端收到客户端的SYN包(SYN=j)后,需要回复一个SYN+ACK的包给客户端。
  2. 这个SYN+ACK的包里,ACK的值为j+1,表示"我已经收到你的SYN了"。
  3. 同时,服务端也会发送自己的SYN包,序列号为ISN(s),这个序列号是服务端自己生成的。

服务端在第二次握手中发送的包,其SYN和ACK标志位都被设置为1(SYN+ACK),序列号(Seq)为服务端自己生成的初始序列号(ISN(s)),确认号(ACK)为客户端的初始序列号加1(ISN(c)+1)。

注意:此处ACK为一个flag标志位,只是说明得到了ACK

在connectionMark类补充ACKMark的初始化

package TCP_handShake;/*** 标志位 connectionMarks*/
public class ConnectionMarks extends initializeISN{//每次建立新连接,将SYN初始化为1private int SYN;//随机private int Seq;private  int ACKMark;public int getACKMark() {return ACKMark;}public void setACKMark(int ACKMark) {this.ACKMark = ACKMark;}public ConnectionMarks() {this.SYN = 1;this.Seq = getISN();this.ACKMark = 0;}public int getSeq() {return Seq;}//setter of SYNpublic Integer getSYN() {return SYN;}
}

第二次握手服务端发送数据:

System.out.println("====================");System.out.println("第二次握手:");System.out.println("正在发送SYN, Seq 和 ACK......");ConnectionMarks connectionMarks = new ConnectionMarks();//第二次握手,返回ACK = ISN + 1;//生成自己的ISN(s)String Seq2 = String.valueOf(connectionMarks.getSeq());//ACK2中的ISN为第一次传过来的ISN(c)+1String ACK2 = String.valueOf(ISN1+ 1);//将ack标志位设为1connectionMarks.setACKMark(1);String SYN2 = connectionMarks.getSYN() + "/" + connectionMarks.getACKMark();//2. 准备数据,一定要转成字节数组String data2 = SYN2 + " " + Seq2 + " " + ACK2;//创建数据,并把数据打包byte[] datas2 = data2.getBytes();DatagramPacket datagramPacket2 = new DatagramPacket(datas2, 0,datas2.length, new InetSocketAddress("localhost",8888));//调用对象发送数据datagramSocket.send(datagramPacket2);

第二次握手客户端接收数据:

System.out.println("====================");System.out.println("接收数据:...");//创建数据包,用于接收数据/*** 在第二次握手中,客户端主要会检查两个方面的内容:* 检查ACK标志位:客户端需要确认服务端发送的确认信息(SYN-ACK)中的ACK标志位是否已设置。ACK标志位表示服务端确认收到了客户端的握手请求。* 检查确认号(ACK):客户端需要检查服务端发送的确认信息中的确认号(ACK)是否正确。确认号应该是服务端发送的初始序列号加1,用于告知服务端它已经正确接收到服务端的数据。*/byte[] bytes = new byte[1024];DatagramPacket datagramPacket2 = new DatagramPacket(bytes, bytes.length);datagramSocket.receive(datagramPacket2);String s = new String(datagramPacket2.getData(), 0, datagramPacket2.getLength());//拆分字符串获取其中的SYN,Seq和ACKString[] strArr = s.split(" ");String[] flag = strArr[0].split("/");//System.out.println(strArr[0]);//检验接收信息是否是满足需求的if (!(Integer.parseInt(flag[1]) != 0&& Integer.parseInt(flag[0]) == 1&& Integer.parseInt(strArr[2]) == ISN1 + 1)){//TODO 异常提醒,非本次连接,如何处理throw new RuntimeException("wrong connection");}System.out.println("通过校验");//解析数据包并且输出显示System.out.println("数据为: " + s);

注意:第一次握手服务端不需要进行校验,但是第二次握手用户端就需要进行校验,ACK标志位是否为1,ACK值是否为ISN(c)+1,SYN值是否为1。

第三次握手

第三次握手,客户端会发送以下三个数据:

  1. ACK标志位应该为1,表示确认收到第二次握手客户端发来的消息。
  2. Seq,值和第二次握手服务端传来的ACK相同
  3. ACK值,为第二次握手服务端传来的ISN(s)+1

第三次握手客户端发送数据:

System.out.println("====================");//第三次握手System.out.println("第三次握手:");System.out.println("正在发送SYN, Seq 和 ACK......");connectionMarks.setACKMark(1);String ackMark = String.valueOf(connectionMarks.getACKMark());String Seq3 = strArr[2];String ACK3 = String.valueOf(Integer.parseInt(strArr[1]) + 1);//2. 准备数据,一定要转成字节数组String data3 = ackMark + " " + Seq3 + " " + ACK3;
//        System.out.println("+++++++++++++++++");
//        System.out.println(ACK3);//创建数据,并把数据打包byte[] datas3 = data3.getBytes();DatagramPacket datagramPacket3 = new DatagramPacket(datas3, 0,datas3.length, new InetSocketAddress("localhost",9999));//调用对象发送数据datagramSocket.send(datagramPacket3);

第三次握手服务端接收数据

 System.out.println("====================");System.out.println("接收数据:...");//创建数据包,用于接收数据byte[] bytes3 = new byte[1024];DatagramPacket datagramPacket3 = new DatagramPacket(bytes3, bytes3.length);datagramSocket.receive(datagramPacket3);String s3 = new String(datagramPacket3.getData(), 0, datagramPacket3.getLength());//解析数据包并且输出显示System.out.println("数据为: " + s3);//拆分字符串获取其中的SYN,Seq和ACKString[] strArr3 = s.split(" ");//System.out.println(strArr[0]);//检验接收信息是否是满足需求的if (Integer.parseInt(strArr3[0]) != 1){//TODO 异常提醒,非本次连接,如何处理throw new RuntimeException("wrong connection");}System.out.println("通过校验,完成三次握手");

初步总结:

至此完成了简单的三次握手,但是并没有实现超时重传机制,MTU输入缓冲。后续会进行完善和修改,全部代码会在我完成整个TCP通信流程后,开源到GitHub,由于作者能力有限可能有一些错误还烦请大家指出来,我会第一时间进行反思和修改,感谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/257062.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

408——知识点大杂烩

在完成专业课的一轮复习以及历年真题的学习后,发现选择题甚至个别大题的考点就单纯考对概念的理解,会就是会,不会想到脑壳疼都做不出来,而408的知识点主打一个多杂,所以过来整理一下笔记。本文的知识点主要是在我做题过…

2002-2021年全国各地级市环境规制18个相关指标数据

2002-2021年全国各地级市环境规制18个相关指标数据 1、时间:2002-2021年 2、来源:城市年鉴 3、指标:行政区划代码、地区、年份、工业二氧化硫排放量(吨)、工业烟粉尘排放量(吨)、工业废水排放量(万吨)、工业废水排放达标量(万吨)、工业二氧…

dell服务器安装PERCCLI

因在linux 系统中无法查看系统磁盘的raid级别,也无法得知raid状态,需要安装额外的包来监控,因是dell服务器,就在dell网站中下载并安装 1、下载链接:驱动程序和下载 | Dell 中国https://www.dell.com/support/home/zh-…

互联网Java工程师面试题·RabbitMQ篇

目录 1、什么是 rabbitmq 2、为什么要使用 rabbitmq 3、使用 rabbitmq 的场景 4、如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息? 5、如何避免消息重复投递或重复消费? 6、消息基于什么传输? 7、消息如…

设计模式基础——概述(1/2)

目录 一、设计模式的定义 二、设计模式的三大类别 三、设计模式的原则 四、主要设计模式目录 4.1 创建型模式(Creational Patterns) 4.2 结构型模式(Structural Patterns) 4.3 行为型模式(Behavioral Patterns&…

Java---线程讲解(二)

文章目录 1. Runnable接口2. 卖票案例3. 同步代码块解决数据安全问题4. 同步方法解决数据安全问题5. 线程安全的类6. Lock锁 1. Runnable接口 1. 创建线程的另一种方法是声明一个实现Runnable接口的类,之后重写run()方法,然后可以分配类的实例&#xff0…

利用eclipse导入外部java工程

利用eclipse导入外部java工程,打开eclipse,依次点击File-Import,…按下图依次执行…

Facebook引流脚本的优势与编写教程!

在当今的数字化时代,社交媒体已经成为企业进行营销和推广的重要渠道之一,Facebook作为全球最大的社交媒体平台之一,拥有数十亿的用户,为企业提供了无限的引流可能性。 然而,对于企业来说,在Facebook上吸引…

uni-app 微信小程序之好看的ui登录页面(四)

文章目录 1. 页面效果2. 页面样式代码 更多登录ui页面 uni-app 微信小程序之好看的ui登录页面(一) uni-app 微信小程序之好看的ui登录页面(二) uni-app 微信小程序之好看的ui登录页面(三) uni-app 微信小程…

AttributeError: module ‘importlib_resources‘ has no attribute ‘path‘ 解决方案

问题描述 with importlib_resources.path("xx", fname) as p: AttributeError: module importlib_resources has no attribute path 博主使用的是python3.9,看importlib_resources在importlib-resources PyPI中的介绍,开始猜测问题出在pyth…

先验概率和后验概率

先验概率(prior probability):是指根据以往经验和分析得到的概率。先验概率不用贝叶斯公式计算。 后验概率(posterior probability):指某个事件已经发生,想要计算这个事件是由于某个因素引起的概…

电子秤ADC芯片CS1237技术资料问题合集

问题11:实际应用中,多个称重传感器应该怎么与ADC连接? 解答:如果传感器是测量同一物体(例如:厨房垃圾处理器),一般建议使用并联的方式。则相同类型的信号线连接在一起。对于传感器的…