TCP协议与UDP协议

目录

一、TCP与UDP的特点

(一)有连接VS无连接

(二)可靠传输VS不可靠传输

(三)面向字节流VS面向数据报

(四)全双工VS半双工

[二、UDP协议中的socket api](#二、UDP协议中的socket api)

(一)DatagramSocket类

(二)DatagramPacket类

(三)InetSocketAddress类

三、UDP协议的回显服务器

(一)UdpEchoServer回显服务器

(二)UdpEchoClient客户端

(三)拓展:英译汉服务器

[四、TCP协议中的socket api](#四、TCP协议中的socket api)

(一)ServerSocket类

(二)Socket类

五、TCP协议的回显服务器

(一)TcpEchoServer回显服务器

(二)TcpEchoClient客户端


一、TCP与UDP的特点

TCP的特点:

  • 有连接
  • 可靠传输
  • 面向字节流
  • 全双工

UDP的特点:

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 全双工

(一)有连接VS无连接

这是抽象的概念,指的是虚拟的/逻辑上的连接。

  • 对于TCP来说,TCP协议中,就保存了对端的信息: A和B通信,A和B先建立连接,让A保存B的信息,B保存A的信息(彼此之间知道要连接的是哪个)。
  • 对于UDP来说,UDP协议本身,不保存对方的信息,就是无连接。

(二)可靠传输VS不可靠传输

在网络上,数据是非常容易出现丢失的情况的(丢包),光信号/电信号都可能受到外界的干扰。

在进行通信时,不能指望一个数据包100%地到达对方。

  • 可靠传输指的是,虽然不能保证数据包100%到达,但是能尽可能提高传输成功的概率。
  • 不可靠传输只是把数据发了,就不管了。

(三)面向字节流VS面向数据报

  • 面向字节流指的是在读写数据时,以字节为单位。
  • 面向数据报指的是读写数据时,以数据报为单位。

(四)全双工VS半双工

  • 全双工指的是 一个通信链路中,支持双向通信(能读也能写)。
  • 半双工指的是 一个通信链路中,只支持单向通信(要么读,要么写)。

二、UDP协议中的socket api

计算机中的"文件",是一个广义的概念,文件还能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件,统一管理的)。

UDP协议是用来操作网卡的,将网卡抽象成socket文件,操作网卡的时候,流程和操作普通文件差不多。

(一)DatagramSocket类

DatagramSocket类是用来操作socket文件,发送和接收数据报的。

构造方法:

|--------------------------|------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到主机的任意一个随机端口号(一般用于客户端)。 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到主机的一个指定的端口号(一般用于服务端)。 |

成员方法:

|--------------------------------|----------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接受到数据报,该方法会阻塞等待)。 |
| void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送)。 |
| void close() | 关闭此数据报套接字。 |

(二)DatagramPacket类

DatagramPacket就是UDP发送和接收的数据报。

构造方法:

|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[]buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据报保存在字节数组中(第一个参数buf),接收的指定长度(第二个参数length)。 |
| DatagramPacket(byte[]buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来接收数据报,接收的数据报保存在字节数组中(第一个参数buf),指定起点(第二个参数offset),接收的指定长度(第三个参数length)。address指定目的主机的IP和端口号。 |

成员方法:

|--------------------------|-----------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端的主机IP地址;或从发送的数据报中,获取接收端的主机IP地址。 |
| int getPort() | 从接收的数据报中,获取发送端的主机的端口号;或从发送的数据报中,获取接收端的主机的端口号。 |
| byte[] getData() | 获取数据报中的数据。 |

(三)InetSocketAddress类

构造UDP发送的数据报时,需要传入SocketAddress(父类),该对象可以使用InetSocketAddress(子类)来创建。

InetSocketAddress的构造方法:

|----------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |

三、UDP协议的回显服务器

Java数据报套接字通信模型:

(一)UdpEchoServer回显服务器

java 复制代码
package NetWork;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

//UDP协议的回显服务器
//服务器端
public class UdpEchoServer {
    private DatagramSocket socket=null;
    //指定了一个固定端口号, 让服务器来使用.
    public UdpEchoServer(int port) throws SocketException {
        socket=new DatagramSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //循环一次,就相当于处理一次请求。
            //1.读取请求并解析
            //创建请求数据报
            DatagramPacket RequestPacket=new DatagramPacket(new byte[4096],4096);
            //开始接收,并更新数据报
            socket.receive(RequestPacket);
            //2.根据请求, 计算响应. (服务器最关键的逻辑)
            //把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
            String request=new String(RequestPacket.getData(),0, RequestPacket.getLength());
            String response=process(request);
            //3.把响应返回给客户端
            //根据 response 构造 DatagramPacket, 发送给客户端.
            //此处不能使用 response.length(),因为这是String的长度而不是byte数组的长度
            DatagramPacket ResponsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,RequestPacket.getSocketAddress());
            //发送构建好的数据报
            socket.send(ResponsePacket);
            //4.打印日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n", RequestPacket.getAddress().toString(), RequestPacket.getPort(), request, response);
        }
    }
    //服务器根据请求,处理业务
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }

}

(二)UdpEchoClient客户端

java 复制代码
package NetWork;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

//UDP协议的回显服务器
//客户端
public class UdpEchoClient {
    DatagramSocket socket=null;
    // 客户端要给服务器发送数据报,首先得知道服务器的IP和端口号
    private String ServerIp;//目的IP
    private int ServerPort;//目的端口号

    // 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.ServerIp = serverIp;
        this.ServerPort = serverPort;
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner sc=new Scanner(System.in);
        while(true){
            // 1.读取用户输入的内容
            System.out.println("请输入要发送的内容:");
            if(!sc.hasNext()){
                break;
            }
            String request=sc.next();
            // 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
            // 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
            DatagramPacket RequestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(ServerIp),ServerPort);
            // 3. 发送数据报
            socket.send(RequestPacket);
            // 4. 接收服务器的响应
            DatagramPacket ResponsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(ResponsePacket);
            // 5. 从服务器读取的数据进行解析, 打印出来.
            String response = new String(ResponsePacket.getData(), 0, ResponsePacket.getLength());
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

(三)拓展:英译汉服务器

当我们需要实现另外一个简单的服务器时,例如英译汉服务器,只需要继承然后重写process方法就可以了。

java 复制代码
package NetWork;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

//英译汉服务器
public class UdpDictServer extends UdpEchoServer{
    private HashMap<String,String> dict=new HashMap<>();
    //要在子类的构造方法中调用父类的构造方法
    //构造方法初始化字典
    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("apple","苹果");
        dict.put("boy","男孩");
        dict.put("cat","小猫");
        dict.put("dog","小狗");
    }
    //重写process方法
    public String process(String request){
        return dict.getOrDefault(request,"没有找到该词汇");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer DictServer=new UdpDictServer(9090);
        DictServer.start();
    }
}

四、TCP协议中的socket api

(一)ServerSocket类

ServerSocket是创建TCP服务器端Socket的API。

构造方法:

|------------------------|-----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务器端套接字Socket,并绑定到指定端口。 |

成员方法:

|-----------------|-------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务器端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待。 |
| void close() | 关闭此套接字 |

(二)Socket类

Socket类是客户端socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

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

构造方法:

|------------------------------|----------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端套接字Socket,并对应IP的主机上对应端口的进程进行连接。 |

成员方法:

|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutPutStream() | 返回此套接字的输出流 |

五、TCP协议的回显服务器

(一)TcpEchoServer回显服务器

java 复制代码
package NetWork;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket=null;

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

    public void start() throws IOException {
        System.out.println("启动服务器");

        // 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.
        ExecutorService executorService = Executors.newCachedThreadPool();

        while (true) {
            // tcp 来说, 需要先处理客户端发来的连接.
            // 通过读写 clientSocket, 和客户端进行通信.
            // 如果没有客户端发起连接, 此时 accept 就会阻塞.

            // 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.
            Socket clientSocket = serverSocket.accept();

            // 使用多线程的方式来调整
            // Thread t = new Thread(() -> {
            // processConnection(clientSocket);
            // });
            // t.start();

            // 使用线程池来调整
            executorService.submit(() -> {
                processConnection(clientSocket);
            });
        }
    }

    private void processConnection(Socket clientSocket){//对clientSocket进行读写操作
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            // 针对 InputStream 套了一层
            Scanner scanner = new Scanner(inputStream);
            // 针对 OutputStream 套了一层
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){//因为输入流中的数据是持续读取的,要加上循环
                // 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.
                if (!scanner.hasNext()) {
                    //scanner.hasNext():判断输入流中是否还有 "下一个令牌"(默认以空白字符分割,如空格、换行等)。
                    // 连接断开了
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 2. 根据请求计算响应
                String request=scanner.next();
                String response=process(request);
                // 3. 返回响应到客户端
                // outputStream.write(response.getBytes());
                writer.println(response);
                //将缓存区中的数据都发送出去,避免残留
                writer.flush();
                // 打印日志
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                //服务器连接一个客户端就要创建一个clientSocket,使用完就要关闭.
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    private String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

(二)TcpEchoClient客户端

java 复制代码
package NetWork;

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 TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 直接把字符串的 IP 地址, 设置进来.
        // 127.0.0.1 这种字符串
        socket = new Socket(serverIp, serverPort);
    }
    public void start()throws IOException{
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            //给inPutStream套一层
            Scanner scannerNet= new Scanner(inputStream);
            //给outPutStream套一层
            PrintWriter writer=new PrintWriter(outputStream);
            while (true){
                //1.读取用户输入
                String request=scanner.next();
                //2.发送请求并刷新缓存区数据
                writer.println(request);
                writer.flush();
                //3.接收服务器的响应
                String response=scannerNet.next();
                //4.打印出响应
                System.out.println(response);
            }

        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

注意点:

  • 在服务器中,采用多线程的方式来处理客户端的请求(使用线程池)。因为如果是单线程有多个客户端连接,当程序处理processConnection请求时,就可能阻塞在processConnection,而不能accpet。
  • 因为服务器中有scanner.hasNext来判断发来的请求,所以客户端发送的请求要以换行符/空白符号结束,因此发送时用writer.println。
  • 发送请求后记得使用flush刷新缓冲区的数据。
相关推荐
EndingCoder2 小时前
HTTP性能优化实战:解决高并发场景下的连接瓶颈与延迟问题
网络·网络协议·http·性能优化·高并发
IPIDEA全球IP代理2 小时前
IPIDEA:全球领先的企业级代理 IP 服务商
网络·网络协议·tcp/ip
kebeiovo2 小时前
网络muduo库的实现(2)
服务器·网络·php
十年一梦实验室2 小时前
工业机器人控制系统 IP-PP-EXEC 流水线
网络·人工智能·网络协议·tcp/ip·机器人
无名的小三轮2 小时前
IP OSPF综合大实验
网络
hello_ world.2 小时前
RHCA04--系统模块管理与资源限制
linux·服务器·网络
信必诺2 小时前
网络 —— 笔记本(主机)、主机虚拟机(Windows、Ubuntu)、手机(笔记本热点),三者进行相互ping通
网络·ubuntu·智能手机
瑞哥-RealWang6 小时前
群晖SA6400/DT机型 如何将USB存储设置为内部存储 (限ESXI方案)
网络·群晖·sa6400
Dream Algorithm6 小时前
室内分布系统
网络