网络通信(二)

UDP通信

特点:无连不是先接、不可靠通信

不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了

java提供了一个java.net.DatagramSocket类来实现UDP通信

DatagramSocket

DatagramSocket 用于创建客户端和服务器端的 UDP 通信。它提供了发送和接收 DatagramPacket 的方法。

构造器:
  • DatagramSocket(int port): 创建绑定到特定端口的 DatagramSocket

  • DatagramSocket(int port, InetAddress laddr): 创建绑定到特定地址和端口的 DatagramSocket

处理数据包方法:
  • send(DatagramPacket p): 发送 DatagramPacket

  • receive(DatagramPacket p): 接收 DatagramPacket

DatagramPacket

DatagramPacket 是用来封装 UDP 数据包的类。它包含数据、源端口、目标端口和 IP 地址。

构造器:
  • DatagramPacket(byte[] buf, int length): 创建一个空的数据包,数据存储在 buf 数组中,长度为 length

  • DatagramPacket(byte[] buf, int length, InetAddress address, int port): 创建一个数据包,指定数据、目标 IP 地址和端口。

创建数据包:
  1. 首先,你需要一个字节数组来存储你的数据。

  2. 然后,使用适当的构造器创建 DatagramPacket 对象。

  3. 对于发送,你需要设置目标 IP 地址和端口。

  4. 对于接收,DatagramSocket.receive() 方法会填充 DatagramPacket 的源 IP 地址和端口。

示例代码

//服务器端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
​
public class UDPServer {
    public static void main(String[] args) throws Exception {
        DatagramSocket serverSocket = new DatagramSocket(12345);//创建服务端对象,指定服务端端口
        byte[] receiveData = new byte[1024];//指定字节数组大小
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);//创建数据包
​
        serverSocket.receive(receivePacket);//数据包接收数据
        //接收到指定长度的字节数组并转为字符串
        String receivedMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.println("Server received: " + receivedMessage);
​
        serverSocket.close();
    }
}
//客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
​
public class UDPClient {
    public static void main(String[] args) throws Exception {
        InetAddress IPAddress = InetAddress.getByName("localhost");//创建客户端对象,指定接收主机IP
        int port = 12345;//指定要发送到主机的服务端端口
​
        String sentence = "Hello World!";
        byte[] buf = sentence.getBytes();//封装数据到字节数组中
        DatagramPacket packetToSend = new DatagramPacket(buf, buf.length, IPAddress, port);
​
        DatagramSocket clientSocket = new DatagramSocket();//创建数据包用于接收数据
        clientSocket.send(packetToSend);//数据包发送数据
​
        clientSocket.close();
    }
}

demo1:实现一发一收

  1. 实现客户端、服务端

  2. 创建客户端对象

  3. 创建数据包对象,封装发出的数据

  4. 开始使用数据包发送数据

  5. 释放资源(数据包)

  6. 创建服务端对象(注册端口)

  7. 创建数据包对象,接收数据

  8. 开始使用数据包接收数据

  9. 从字节数组中,把本次接收的数据直接打印出来(注意:可能多次接收数据,字节数组被多次不完全覆盖,因此需要接收多少就倒多少)

  10. (最后,可以通过socket获取客户端的IP地址)

demo2:实现多发多收

直接对demo1进行改造:

  1. 实现客户端时,直接在客户端发送信息的代码块处放入循环中

  2. 注意:此时,客户端只能指定的发一两段信息,因此,直接实现让客户从键盘输入

  3. 实现服务端时,直接在接收处代码块放入死循环中

  4. 注意:服务端的资源一定要在最后关闭,也就是说,资源关闭不包括在循环中,或者说,一般服务端的的资源不会关闭

  5. (最后,可以通过socket获取客户端的IP地址)

TCP通信

特点:面向连接、可靠通信

通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。

java提供了一个java.net.Socket类和ServerSocket类来实现TCP通信

模型:

在 Java 中,Socket 类是用于建立 TCP(传输控制协议)网络连接的。TCP 是一种面向连接的、可靠的、基于字节流的协议,它确保数据以发送顺序到达目的地。

Socket 类

Socket 类位于 java.net 包中,它代表了一个客户端的 TCP 连接端点。

构造方法

Socket 类有几个构造方法,用于以不同的方式创建套接字:

  1. 无参构造方法:

    Socket()

    创建一个新的未连接的套接字。

  2. 指定服务器地址和端口的构造方法:

    Socket(InetAddress address, int port)
    Socket(String host, int port)

    这两个构造方法都会创建一个新的套接字并尝试连接到指定的 IP 地址和端口,或者主机名和端口。

  3. 指定服务器地址、端口和本地端口的构造方法:

    Socket(InetAddress address, int port, InetAddress localAddress, int localPort)
    Socket(String host, int port, InetAddress localAddress, int localPort)

    这些构造方法允许你指定本地绑定的地址和端口,以及远程服务器的地址和端口。

  4. 指定服务器地址、端口、超时时间和操作模式的构造方法:

    Socket(InetAddress address, int port, boolean stream)
    Socket(String host, int port, boolean stream)

    这些构造方法允许你指定是否使用流模式(对于 TCP,通常设置为 true)。

数据处理方法

Socket 类提供了几个方法来处理数据:

  1. 获取输入流:

    InputStream getInputStream()

    返回一个输入流,用于从套接字读取数据。

  2. 获取输出流:

    OutputStream getOutputStream()

    返回一个输出流,用于向套接字写入数据。

  3. 关闭套接字:

    void close()

    关闭套接字,释放系统资源。

  4. 连接服务器:

    void connect(SocketAddress endpoint)
    void connect(SocketAddress endpoint, int timeout)

    这些方法用于建立到指定套接字地址的连接,可选地指定连接超时时间。

  5. 设置套接字选项:

    • setKeepAlive(boolean on): 设置 TCP 保活机制。

    • setReuseAddress(boolean on): 允许本地地址被重用。

    • setSoLinger(boolean on, int linger): 设置套接字的 SO_LINGER 选项,控制关闭套接字时的行为。

示例代码

客户端:

import java.io.*;
import java.net.*;
​
public class TCPClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 1234)) {
            OutputStream output = socket.getOutputStream();
            output.write("Hello Server!".getBytes());
​
            InputStream input = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int bytesRead = input.read(buffer);
            System.out.println(new String(buffer, 0, bytesRead));
​
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
服务器端:

import java.io.*;
import java.net.*;
​
public class TCPServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(1234)) {
            Socket socket = serverSocket.accept();
            InputStream input = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int bytesRead = input.read(buffer);
            System.out.println(new String(buffer, 0, bytesRead));
​
            OutputStream output = socket.getOutputStream();
            output.write("Hello Client!".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这些示例中,客户端创建了一个 Socket 对象来连接服务器,并使用输入输出流来发送和接收数据。服务器端使用 ServerSocket 来监听连接请求,并接受客户端的连接,然后也使用输入输出流来处理数据。

demo1:实现一发一收<单客户端>

  1. 创建客户端的Socket对象(套接字对象),请求与 服务端的连接

  2. 使用socket对象调用getOutputStream()方法得到字节输出流

  3. 使用数据输出流(其他高级流也行,这个最适合)完成数据的发送

  4. 释放资源:关闭socket管道

  5. 创建ServerSocket的对象,同时为服务端注册端口

  6. 使用serverSocket对象,调用accept方法,等待客户端的连接请求得到socket对象

  7. 从socket通信管道中得到一个字节输入流

  8. 把原始的字节输入流包装为数据输入流(其他高级流也行,这个最适合)

  9. 使用数据输入流读取客户端发送的消息

    注意:接收数据时数据输入流对象调用的方法和数据输出流对象调用的方法相对应

    (最后,可以通过socket获取客户端的IP地址)

package Wechat;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

public class Cilent {
    //1.客户端
    public static void main(String[] args) throws Exception {
//        1. 创建客户端的Socket对象(套接字对象),请求与 服务端的连接
        Socket socket = new Socket(InetAddress.getLocalHost(),8888);//"127.0.0.1"
//        2. 使用socket对象调用getOutputStream()方法得到字节输出流
        OutputStream os = socket.getOutputStream();
//        3. 使用数据输出流(其他高级流也行,这个最适合)完成数据的发送
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeUTF("客户端发送成功!");
//        4. 释放资源:关闭socket管道
        dos.close();
        socket.close();

    }
}


package Wechat;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception {
//        5. 创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
//        6. 使用serverSocket对象,调用accept方法,等待客户端的连接请求得到socket对象
        Socket socket = serverSocket.accept();
//        7. 从socket通信管道中得到一个字节输入流
        InputStream is = socket.getInputStream();
//        8. 把原始的字节输入流包装为数据输入流(其他高级流也行,这个最适合)
        DataInputStream dis = new DataInputStream(is);
//        9. 使用数据输入流读取客户端发送的消息
        String message = dis.readUTF();
        System.out.println(message);
//        注意:接收数据时数据输入流对象调用的方法和数据输出流对象调用的方法相对应
//   (最后,可以通过socket获取客户端的IP地址)
        System.out.println("发消息的用户为:" + socket.getRemoteSocketAddress());
    }
}

demo2:实现多发多收<单客户端>

改造demo1:

  1. 将客户端写数据部分代码块放入死循环

  2. 将死循环内改造为用户键盘输入(发送后,调用flush方法刷新立即写出)

  3. 将服务端读取客户端消息放入死循环,并把关闭套接字去掉,实现多收

  4. 注意:如果客户端离线,服务端处会出现异常,因此在服务端捕获异常,就可以知道客户端的在线离线状态

package Wechat;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

public class Cilent {
    //1.客户端
    public static void main(String[] args) throws Exception {
//        1. 创建客户端的Socket对象(套接字对象),请求与 服务端的连接
        Socket socket = new Socket(InetAddress.getLocalHost(),8888);//"127.0.0.1"
//        2. 使用socket对象调用getOutputStream()方法得到字节输出流
        OutputStream os = socket.getOutputStream();
//        3. 使用数据输出流(其他高级流也行,这个最适合)完成数据的发送
        DataOutputStream dos = new DataOutputStream(os);
        //需要实现用户键盘输入什么发送什么,输入"exit"时退出
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请输入信息:");
            String message = sc.nextLine();
            if("exit".equals(message)){
                System.out.println("客户端已退出!");
                break;
            }
            dos.writeUTF(message);
            dos.flush();
            dos.writeUTF("客户端发送成功!");
        }
    }
}

package Wechat;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    //服务端
    public static void main(String[] args) throws Exception {
//        5. 创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
//        6. 使用serverSocket对象,调用accept方法,等待客户端的连接请求得到socket对象
        Socket socket = serverSocket.accept();
//        7. 从socket通信管道中得到一个字节输入流
        InputStream is = socket.getInputStream();
//        8. 把原始的字节输入流包装为数据输入流(其他高级流也行,这个最适合)
        DataInputStream dis = new DataInputStream(is);
        while (true) {
//        9. 使用数据输入流读取客户端发送的消息
            try {
                //等待客户端发送消息,如果客户端下线会出现异常,此时应该捕获异常并提示
                String message = dis.readUTF();
                System.out.println(message);
                System.out.println("发消息的用户为:" + socket.getRemoteSocketAddress());
                System.out.println("------------------------------");
            } catch (IOException e) {
                System.out.println("有用户下线了:" + socket.getRemoteSocketAddress());
                break;
            }
        }
    }
}

支持服务端与多个客户端同时通信【客户端多开】

改造demo2:

  1. 由于需要每一个套接字都需要一个线程负责,所以需要专门写一个服务端的读取线程类,用于接收并处理套接字

  2. 主要核心的接收信息功能就在run方法中写了

  3. 主线程中拿到套接字时可以适当的输出客户端上线状态

  4. run方法中也可以添加客户端的下线状态(客户端的下线会使得服务端的接收消息处一直等待发生异常,因此需要捕获异常----提示客户端下线)

package Wechat;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

public class Cilent {
    //1.客户端
    public static void main(String[] args) throws Exception {
//        1. 创建客户端的Socket对象(套接字对象),请求与 服务端的连接
        Socket socket = new Socket(InetAddress.getLocalHost(),8888);//"127.0.0.1"
//        2. 使用socket对象调用getOutputStream()方法得到字节输出流
        OutputStream os = socket.getOutputStream();
//        3. 使用数据输出流(其他高级流也行,这个最适合)完成数据的发送
        DataOutputStream dos = new DataOutputStream(os);
        //需要实现用户键盘输入什么发送什么,输入"exit"时退出
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请输入信息:");
            String message = sc.nextLine();
            if("exit".equals(message)){
                System.out.println("客户端已退出!");
                break;
            }
            dos.writeUTF(message);
            dos.flush();
            dos.writeUTF("客户端发送成功!");
        }
    }
}

package Wechat;

import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception {
//        5. 创建ServerSocket的对象,同时为服务端注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        while (true) {
//        6. 使用serverSocket对象,调用accept方法,等待客户端的连接请求得到socket对象
            Socket socket = serverSocket.accept();//拿到套接字
            System.out.println("有用户上线了:" + socket.getRemoteSocketAddress());
            new ServerReaderThread(socket).start();//把套接字交给子线程并启动
        }
    }
}

package Wechat;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ServerReaderThread extends Thread{
    private Socket socket;
    //每个子线程拿到套接字
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }

    //负责描述子线程中该套接字的任务
    @Override
    public void run() {
        //该套接字负责读取客户端的信息;
        while (true) {
            try {
                InputStream is = socket.getInputStream();
                DataInputStream dis = new DataInputStream(is);
                String message = dis.readUTF();
                System.out.println(message);
                System.out.println("-------------------------");
            } catch (IOException e) {
                System.out.println("有用户下线:" + socket.getRemoteSocketAddress());
                try {
                    socket.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                break;
            }
        }
    }
}

问题:

为什么UDP通信可以单线程实现多客户端收发信息,但是TCP通信不可以单线程实现多客户端收发信息?

答:

UDP(用户数据报协议)和TCP(传输控制协议)在设计上有着本质的区别,这导致了它们在处理多客户端通信时的不同方式。

UDP通信单线程模型

UDP 是无连接的协议,它的主要特点是简单和高效。在 UDP 中,数据以数据报的形式发送,每个数据报都是独立的,不保证数据报的顺序、可靠性或完整性。因此,UDP 通信可以很容易地使用单线程模型来处理多个客户端:

  1. 无状态:UDP 服务器不需要维护任何关于客户端状态的信息,每个数据报都是独立的,服务器可以简单地接收和发送数据报,而不需要跟踪任何特定的客户端。

  2. 并发处理:由于 UDP 是无连接的,服务器可以同时接收来自多个客户端的数据报,并且不需要为每个客户端创建独立的连接或线程。

  3. 简单性:UDP 的简单性使得它很容易实现一个单线程的服务器,该服务器可以循环接收数据报,并根据需要广播或转发给其他客户端。

TCP通信多线程或非阻塞模型

TCP 是面向连接的协议,它提供了数据的顺序、可靠性和完整性保证。TCP 连接需要经过三次握手过程来建立,并且在连接期间,服务器需要维护每个客户端的状态信息:

  1. 连接状态:TCP 服务器需要为每个客户端维护一个连接状态,这包括序列号、确认号、窗口大小等信息。

  2. 顺序保证:TCP 保证数据的顺序,因此服务器需要确保数据按照正确的顺序发送和接收。

  3. 流量控制和拥塞控制:TCP 需要处理流量控制和拥塞控制,这需要服务器为每个连接维护额外的状态信息。

  4. 资源消耗:由于 TCP 连接需要更多的资源和处理,因此当客户端数量增加时,单线程模型可能会成为性能瓶颈。

  5. 并发限制:单线程处理多个 TCP 连接可能会导致性能问题,因为每个连接都需要进行数据的读取和写入,这可能会阻塞线程,影响其他连接的处理。

因此,为了有效地处理多个 TCP 客户端,通常需要使用多线程模型或非阻塞 I/O(如 Java 的 NIO):

  • 多线程模型:为每个客户端连接创建一个新线程,这样可以并行处理多个连接,但可能会消耗大量资源。

  • 非阻塞 I/O:使用非阻塞 I/O 和事件驱动的方法,如 Java 的 NIO(New I/O)库,可以更高效地处理多个连接,而不需要为每个连接创建一个线程。

总之,UDP 的无连接特性和简单性使其适合于单线程模型,而 TCP 的面向连接特性和对可靠性的保证使其更适合于多线程或非阻塞 I/O 模型。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值