一文理解TCP与UDP


Socket套接字

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。 基于Socket套接字的网络程序开发就是网络编程。

Socket套接字主要针对传输层协议划分为如下三类:

  1. 流套接字:使用传输层TCP协议
  2. 数据报套接字:使用传输层UDP协议
  3. 原始套接字

那么这三种协议有什么不同呢?接下来我们就一个一个认识,

传输层TCP协议

TCP,即Transmission Control Protocol(传输控制协议),传输层协议。

以下为TCP的特点:

  1. 有连接
  2. 可靠传输
  3. 面向字节流
  4. 有接收缓冲区,也有发送缓冲区
  5. 大小不限

注意:下文会讲解这些特点到底是什么意思。

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的 情况下,是⽆边界的数据,可以多次发送,也可以分开多次接收。

传输层UDP协议

UDP,即User Datagram Protocol(用户数据报协议),传输层协议。 以下为UDP的特点

  1. 无连接
  2. 不可靠传输
  3. 面向数据报
  4. 有接收缓冲区,无发送缓冲区
  5. 大小受限:一次最多传输64k

原因: 我们注意到, UDP协议首部中有⼀个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是 64K(包含UDP首部).

  1. 注意:下文会讲解这些特点到底是什么意思。

对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须⼀ 次发送,接收也必须⼀次接收100个字节,⽽不能分100次,每次接收1个字节。

至于第三种就不详细讲了,想了解的可以自己找资料来了解哦。这里就简单讲作用:

原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。 我们不学习原始套接字,简单了解即可。

下面讲解一下上面tcp所说的特点:

  1. **有连接:**在数据传输开始之前,必须在两端建立一个连接。这通过一个称为三次握手的过程完成。
  2. 可靠性:TCP确保数据正确无误地从源传输到目的地。它通过序列号、确认应答、重传机制等来实现。但也不是绝对安全的,只是在传输中尽量保证数据正确无误。
  3. 全双工通信:TCP允许通信双方同时发送和接收数据。可以简单理解为:可以从左到右,也可从右到左,左右代表通信双方
  4. 字节流:虽然应用程序可能发送的是消息或数据块,但TCP将它们视为字节流。后面代码中就会发现我们使用TCP网络编程时会使用到输出流和输入流即:InputStream和OutputStream。
  5. 面向数据包:UDP是面向报文的,只在应用层交下来的报文前增加了首部后就向下交付网络层。它指的是协议在传输数据时将数据封装成一个个独立的数据单元,这些数据单元被称为数据包。

当然上面对两种协议的解释是比较简陋的。


UDP数据报套接字

想要学会实现UDP数据报套接字,那就先要认识两个类 DatagramSocket和 DatagramPacket (也可以说是两个api)

两个类的主要作用:

DatagramSocket:用于发送和接收数据的。

DatagramPacket: 是DatagramSocket发送和接收的数据报。

一个形象的比喻:一个是快递车(DatagramSocket),一个是包裹(DtagramPacket)


DtagramPacket的主要方法:

**DatagramPacket(byte[] buf, int length):**构造⼀个DatagramPacket以⽤来接收数据报,接收的 数据保存在字节数组(第⼀个参数buf)中,接收指定 ⻓度(第⼆个参数length)

DatagramPacket(byte[] buf, int offset, int length, SocketAddress address): 构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓ 度(第⼆个参数length)。address指定⽬的主机的IP 和端⼝号


DtagramSocket的主要方法:

**InetAddress getAddress() :**从接收的数据报中,获取发送端主机IP地址;或从发 送的数据报中,获取接收端主机IP地址

**int getPort():**从接收的数据报中,获取发送端主机的端口号;或从 发送的数据报中,获取接收端主机端口号


下面就是服务端和客户端实现一个简单的回响服务端(即收到并返回客户端发来的请求)和客户端

服务端:

首先我们就需要先构造一个DatagramSocket,我们这里就先创建一个没有实例化的DatagramSocket socket,然后在构造方法中实例化这个socket对象,并且我们就传入一个端口号port通过调用socket的带参数的方法进行设置该服务端的端口号。

然后设置一个启动服务端的方法start(),在start中我们的服务器上线,然后我们需要用一个while循环不断区接收来自客户端的请求。这里我们先初始化一个DatagramPacket对象,然后调用DatagramSocket的receive方法并把DatagramPacket对象传入进方法中进行获取请求,这时请求就是储存在这个DatagramPacket中。

再然后把DatagramPacket对象的数据通过new String(packet.getData(),0, packet.getLength())获取String类型的请求,并通过process处理请求获取响应respond。

最后将String类型的respond转化为一个DatagramPacket对象的数据(相当于打包包裹)这里的包裹与接收不一样,发送时需要加上要发送去的目标客户端的ip和端口号,我们可以使用packet.getSocketAddress()获取这两样东西放入构造方法中即可,然后交给快递车发货(即:DatagramSocket)

复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpService {

    DatagramSocket socket;


    public UdpService(int port) throws SocketException {

        socket=new DatagramSocket(port);

    }


    public void start() throws IOException {


        System.out.println("服务器启动");

        while(true){

            DatagramPacket packet=new DatagramPacket(new byte[4096],4096);
            socket.receive(packet);

            String request=new String(packet.getData(),0, packet.getLength());

            String respond=process(request);

            DatagramPacket packetrespond=new DatagramPacket(respond.getBytes(),0,respond.getBytes().length,packet.getSocketAddress());

            socket.send(packetrespond);
            System.out.println("port:"+packetrespon.getPort()+"ip"+packetrespon.getAddress());




        }

    }

    private String process(String request) {

        return request;
    }


    public static void main(String[] args) throws IOException {

        UdpService service=new UdpService(9090);

        service.start();



    }

}

TCP流套接字

在TCP流套接字编程中,我们要学习到两个比较重要的类(也可以说是API),那么是哪两个类呢,接下来我们就来学习一下吧。

这两个类分别是

  • ServerSocket:主要是服务端使用,是创建TCP服务端Socket的API。
  • Socket:无论是服务端还是客户端,都是需要使用这个类。

ServerSocket类

ServerSocket的构造方法是需要传入一个端口号, ServerSocket(int port) ;用来作为这个服务端的端口号。

ServerSocket常用的方法就是: Socket accept() ,其返回值为Socket

accept作用: 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端 连接后,返回⼀个服务端Socket对象,并基于该 Socket建⽴与客⼾端的连接,否则阻塞等待

Socket类

Socket的构造方法需要传入两个值, Socket(String host, int port) ,一个String以及int类型,分别代表要访服务器的IP地址和服务器端口号。

不管是客户端还是服务端Socket,都是双方建立链接以后,保存的对端信息,以及用来与对方接受数据的。

Socket本身是是无法直接传输数据的,但是它可以通过本身的方法获取对应的流对象,通过流对象进行,请求和反应。以下是Socket的一些方法:

InetAddress getInetAddress() 返回套接字所连接的地址

InputStream getInputStream() 返回此套接字的输⼊流

OutputStream getOutputStream() 返回此套接字的输出流

简单模拟实现一下服务端和客户端:

服务端:

复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpServer {

    private ServerSocket serverSocket=null;

    public TcpServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {

        ExecutorService executorService= Executors.newCachedThreadPool();


        while(true){
            Socket socket=serverSocket.accept();

                executorService.submit(()->{

                    processConnection(socket);

                });

        }

    }

    private String process(String request) {

        return request;
    }


    private void processConnection(Socket socket)  {


            System.out.println("客户端上线");

           try(InputStream inputStream=socket.getInputStream();
               OutputStream outputStream=socket.getOutputStream()){

               //套一层壳
               Scanner sc=new Scanner(inputStream);
               PrintWriter writer=new PrintWriter(outputStream);

               while(true) {
                   if (!sc.hasNext()) {
                       System.out.println("客户端下线");
                       break;

                   }


                   //套壳读请求
                   String request = sc.next();
                   //套壳回响
                   String respond = process(request);


                   writer.println(respond);

                   writer.flush();

                   System.out.println("已经回响"+"ip:"+socket.getInetAddress());
               }

               } catch (IOException e) {
                   throw new RuntimeException(e);
               }finally {
                   try {
                       serverSocket.close();
                   } catch (IOException e) {
                       throw new RuntimeException(e);
                   }

               }


               }


    public static void main(String[] args) throws IOException {
       TcpServer tcpServer=new TcpServer(9090);
       tcpServer.start();

    }

        }

服务端中:

首先创建一个TcpServer类,把这个对象作为一个服务端,这个类的成员属性就是上面所说的ServerSocket类对象开始时我们将这个类对象初始化为null,在构造方法时传入一个端口号对ServerSocket成员在进行实例化如: serverSocket=new ServerSocket(port),在这个类中我们需要创建一个start();方法作为服务器启动的一个方法。在start方法中,我们里面需要到调用一个processConnection方法,这个方法是主要处理一次链接的请求,也可以理解为一个客户端链接请求。

讲完了TcpServer里该有的基本成员,那么我们就讲讲具体的方法是如何实现的:

在start中我们先是建了一个线程池用来服务多用户一起访问这个服务器,发出请求。再使用一个while循环,不断的执行ServerSocket的accept方法进行获取Socket对象,每个线程执行processConnection方法,并将accept获取的Socket对象传给processConnection方法,开始处理一个客户端的请求并响应。

再然后就到了我们的processConnection方法,在这个方法中我们可以先是打印一个客户端上线,然后通过socket对象建立输出,输入流对象,然后可以选择用Scanner和PrintWrite进行套壳使用,然后我们通过scanner读取到来自客户端得到的请求,然后再通过一个process方法进行处理,转化返回对应得响应(这里我们主要是简单的回响服务器,即简单再返回请求的字符串即可),然后我们再使用套壳的PrintWrite对象进行给客户端响应即里的println方法,响应给服务端即可。注意的是write在执行完println后需要在执行flush方法,将写入减缓冲区的响应彻底清除响应给客户端。以及最后记得在finally中关闭Socket对象。与UDP不同,主要看生命周期。

通过主要的这三步,我们基本可以构造一个简单的回响服务端,如果想要更新成更加高级的服务器,那么我们只需要改动一下process方法即可,把里面的逻辑改一下,不在是回响请求即可。

客户端:

复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpClient {
    private Socket socketClient=null;

    public TcpClient(String ip,int port) throws IOException {
        socketClient=new Socket(ip,port);

    }

    public void start(){

        Scanner sc=new Scanner(System.in);
        try(InputStream inputStream=socketClient.getInputStream();
            OutputStream outputStream=socketClient.getOutputStream()){
            PrintWriter writer=new PrintWriter(outputStream);
            Scanner scanner=new Scanner(inputStream);
            while(true){

                System.out.println("请输入你的需求");
                String request=sc.next();


                writer.println(request);

                writer.flush();

                String respond=scanner.next();

                System.out.println(respond);

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }


    public static void main(String[] args) throws IOException {
        TcpClient tcpClient=new TcpClient("127.0.0.1",9090);

        tcpClient.start();
    }

}

客户端:

  1. 客户端我们还是构造一个一个类TcpClient,然后里面先是初始化一个null的Socket对象,然后在带参的构造方法中进行实例化,其中参数分别是服务端的ip和端口号。
  2. 那么还是老样子,定义一个start方法,进行启动客户端,在里面我们还是先创建出输出流和输入流 在进行Scanner和PrintWrite进行套壳,其中我们还要额外创建一个scanner对象里面传入的是System.in进行读取控制台输入的请求,再由输入流传给服务端,然后输出流PrintWrite进行接受响应,注意write这里不要使用print,因为在服务端中我们使用了!sc.hasNext(),如果我们的请求使用的print而不是println则会使服务端读取请求时出现一些小bug。
相关推荐
2401_888859716 分钟前
STM32 I2C硬件读写
stm32·单片机·嵌入式硬件
LaoZhangGong12342 分钟前
测试W5500的第4步_使用ioLibrary库创建UDP客户端和服务器端
网络·经验分享·stm32·嵌入式硬件·网络协议·udp·以太网
XY.散人1 小时前
初识Linux · NAT 内网穿透 内网打洞 代理
linux·服务器·网络
CodeBlossom1 小时前
java加强 -网络编程
网络
AORO_BEIDOU1 小时前
遨游科普:三防平板有哪些品牌?哪个品牌值得推荐?
网络·5g·安全·电脑
widder_1 小时前
软考中级软件设计师——计算机网络篇
网络·计算机网络·智能路由器
小诸葛的博客1 小时前
Flannel后端为UDP模式下,分析数据包的发送方式(二)
网络·网络协议·udp
qq_7391753692 小时前
开源STM32F429汽车仪表盘基于LVGL界面
stm32·嵌入式硬件·汽车
Dontla2 小时前
服务器网络配置 netplan一个网口配置两个ip(双ip、辅助ip、别名IP别名)
服务器·网络·tcp/ip
ip小哥2 小时前
网络世界的“变色龙“:动态IP如何重构你的数据旅程?
网络·tcp/ip·重构