java基础完结——网络编程(2)

1、基于TCP协议的网络编程

TCP(Transmission Control Protocol,传输控制协议)被称作一种端对端协议。是一种面向连接的、可靠的、基于字节流的传输层的通信协议,可以连续传输大量的数据。类似于打电话的效果。

这是因为它为当一台计算机需要与另一台远程计算机连接时,TCP协议会采用“三次握手”方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕TCP协议会采用“四次挥手”方式断开连接。

TCP协议负责收集这些信息包,并将其按适当的次序放好传送,在接收端收到后再将其正确的还原。TCP协议保证了数据包在传送中准确无误。TCP协议使用重发机制,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。

1.1、基于TCP协议的网络通信程序结构

Java语言的基于套接字TCP编程分为服务端编程和客户端编程,其通信模型如图所示:

在这里插入图片描述

服务器程序的工作过程包含以下五个基本的步骤:

  1. 使用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。

  2. 调用 accept()方法:监听连接请求,如果客户端请求连接,则接受连接,创建与该客户端的通信套接字对象。否则该方法将一直处于等待状态。

  3. 调用 该Socket对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收。

  4. 关闭Socket对象:某客户端访问结束,关闭与之通信的套接字。

  5. 关闭ServerSocket:如果不再接收任何客户端的连接的话,调用close()进行关闭。

客户端Socket的工作过程包含以下四个基本的步骤:

  1. 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象,创建的同时会自动向服务器方发起连接。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。

  2. 打开连接到Socket 的输入/出流:使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输。

  3. 进行读/写操作:通过输入流读取服务器发送的信息,通过输出流将信息发送给服务器。

  4. 关闭 Socket:断开客户端到服务器的连接

注意:客户端和服务器端在获取输入流和输出流时要对应,否则容易死锁。例如:客户端先获取字节输出流(即先写),那么服务器端就先获取字节输入流(即先读);反过来客户端先获取字节输入流(即先读),那么服务器端就先获取字节输出流(即先写)。

1.2、案例:一个客户端与服务器单次通信

需求:客户端连接服务器,连接成功后给服务发送“lalala”,服务器收到消息后,给客户端返回“欢迎登录”

服务器端程序示例代码:

package com.bdit.tcp.one;

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

public class Server {
    
    

    public static void main(String[] args)throws Exception {
    
    
        //1、准备一个ServerSocket对象,并绑定8888端口
        ServerSocket server =  new ServerSocket(8888);
        System.out.println("等待连接....");

        //2、在8888端口监听客户端的连接,该方法是个阻塞的方法,如果没有客户端连接,将一直等待
        Socket socket = server.accept();
        System.out.println("一个客户端连接成功!!");

        //3、获取输入流,用来接收该客户端发送给服务器的数据
        InputStream input = socket.getInputStream();
        //接收数据
        byte[] data = new byte[1024];
        StringBuilder s = new StringBuilder();
        int len;
        while ((len = input.read(data)) != -1) {
    
    
            s.append(new String(data, 0, len));
        }
        System.out.println("客户端发送的消息是:" + s);

        //4、获取输出流,用来发送数据给该客户端
        OutputStream out = socket.getOutputStream();
        //发送数据
        out.write("欢迎登录".getBytes());
        out.flush();

        //5、关闭socket,不再与该客户端通信
        //socket关闭,意味着InputStream和OutputStream也关闭了
        socket.close();

        //6、如果不再接收任何客户端通信,可以关闭ServerSocket
        server.close();
    }
}

客户端程序示例代码:

package com.bdit.tcp.one;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
    
    

    public static void main(String[] args) throws Exception {
    
    
        // 1、准备Socket,连接服务器,需要指定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);

        // 2、获取输出流,用来发送数据给服务器
        OutputStream out = socket.getOutputStream();
        // 发送数据
        out.write("lalala".getBytes());
        //会在流末尾写入一个“流的末尾”标记,对方才能读到-1,否则对方的读取方法会一致阻塞
        socket.shutdownOutput();

        //3、获取输入流,用来接收服务器发送给该客户端的数据
        InputStream input = socket.getInputStream();
        // 接收数据
        byte[] data = new byte[1024];
        StringBuilder s = new StringBuilder();
        int len;
        while ((len = input.read(data)) != -1) {
    
    
            s.append(new String(data, 0, len));
        }
        System.out.println("服务器返回的消息是:" + s);

        //4、关闭socket,不再与服务器通信,即断开与服务器的连接
        //socket关闭,意味着InputStream和OutputStream也关闭了
        socket.close();
    }
}

1.3、案例:多个客户端与服务器之间的多次通信

通常情况下,服务器不应该只接受一个客户端请求,而应该不断地接受来自客户端的所有请求,所以Java程序通常会通过循环,不断地调用ServerSocket的accept()方法。

如果服务器端要“同时”处理多个客户端的请求,因此服务器端需要为每一个客户端单独分配一个线程来处理,否则无法实现“同时”。

案例需求:多个客户端连接服务器,并进行多次通信

●每一个客户端连接成功后,从键盘输入英文单词或中国成语,并发送给服务器

● 服务器收到客户端的消息后,把词语“反转”后返回给客户端

● 客户端接收服务器返回的“词语”,打印显示

● 当客户端输入“stop”时断开与服务器的连接

● 多个客户端可以同时给服务器发送“词语”,服务器可以“同时”处理多个客户端的请求

在这里插入图片描述

思考分析:服务器端要“同时”处理多个客户端的请求,那么必须使用多线程,每一个客户端的通信需要单独的线程来处理。

客户端程序示例代码:

package com.bdit.tcp.echo;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    
    

    public static void main(String[] args) throws Exception {
    
    
        // 1、准备Socket,连接服务器,需要指定服务器的IP地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);

        // 2、获取输出流,用来发送数据给服务器
        OutputStream out = socket.getOutputStream();
        PrintStream ps = new PrintStream(out);

        // 3、获取输入流,用来接收服务器发送给该客户端的数据
        InputStream input = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(input));

        Scanner scanner = new Scanner(System.in);
        while(true){
    
    
            System.out.println("输入发送给服务器的单词或成语:");
            String message = scanner.nextLine();
            if(message.equals("stop")){
    
    
                socket.shutdownOutput();
                break;
            }

            // 4、 发送数据
            ps.println(message);
            // 接收数据
            String feedback  = br.readLine();
            System.out.println("从服务器收到的反馈是:" + feedback);
        }

        //5、关闭socket,断开与服务器的连接
        scanner.close();
        socket.close();
    }
}

服务器端程序示例代码:

package com.bdit.tcp.echo;

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

public class Server {
    
    

    public static void main(String[] args)throws Exception {
    
    
        // 1、准备一个ServerSocket
        ServerSocket server = new ServerSocket(8888);
        System.out.println("等待连接...");

        int count = 0;
        while(true){
    
    
            // 2、监听一个客户端的连接
            Socket socket = server.accept();
            System.out.println("第" + ++count + "个客户端连接成功!!");

            ClientHandlerThread ct = new ClientHandlerThread(socket);
            ct.start();
        }

        //这里没有关闭server,永远监听
    }
}

服务器端处理客户端请求的线程类示例代码:

package com.bdit.tcp.echo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class ClientHandlerThread extends Thread{
    
    
    private Socket socket;

    public ClientHandlerThread(Socket socket) {
    
    
        super();
        this.socket = socket;
    }

    public void run(){
    
    

        try{
    
    
            //(1)获取输入流,用来接收该客户端发送给服务器的数据
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //(2)获取输出流,用来发送数据给该客户端
            PrintStream ps = new PrintStream(socket.getOutputStream());
            String str;
            // (3)接收数据
            while ((str = br.readLine()) != null) {
    
    
                //(4)反转
                StringBuilder word = new StringBuilder(str);
                word.reverse();

                //(5)返回给客户端
                ps.println(word);
            }
        }catch(Exception  e){
    
    
            e.printStackTrace();
        }finally{
    
    
            try {
    
    
                //(6)断开连接
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

1.4、案例:多个客户端上传文件

需求:每一个客户端启动后都可以给服务器上传一个文件,服务器接收到文件后保存到一个upload目录中。可以同时接收多个客户端的文件上传。

思考分析:

(1)服务器端要“同时”处理多个客户端的请求,那么必须使用多线程,每一个客户端的通信需要单独的线程来处理。

(2)服务器保存上传文件的目录只有一个upload,而每个客户端给服务器发送的文件可能重名,所以需要保证文件名的唯一。我们可以使用“时间戳”作为文件名,而后缀名不变

(3)客户端需要给服务器上传文件名(含后缀名)以及文件内容。而文件名是字符串,文件内容不一定是纯文本的,因此选择DataOutputStream和DataInputStream。

客户端的示例代码:

package com.bdit.tcp.fileupload;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // (1)连接服务器
        Socket socket = new Socket("192.168.34.53", 8888);

        Scanner input = new Scanner(System.in);

        //(2)从键盘输入文件的路径和名称
        System.out.print("请选择要上传的文件:");
        String path = input.nextLine();
        File file = new File(path);

        OutputStream out = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(out);//用它的目的是为了既可以单独传一个字符串,又可以写字节内容

        //先发送文件名(含后缀名)
        dos.writeUTF(file.getName());//单独发一个字符串

        //还需要一个IO流,从文件读取内容,给服务器发过去
        FileInputStream fis = new FileInputStream(file);

        //(3)把文件内容给服务器传过去,类似与复制文件
        byte[] data = new byte[1024];
        while(true){
    
    
            int len = fis.read(data);
            if(len==-1){
    
    
                break;
            }
            dos.write(data, 0, len);
        }
        socket.shutdownOutput();

        //(4)接收服务器返回的结果
        InputStream is = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);//把字节流转成字符流
        BufferedReader br = new BufferedReader(isr);
        String result = br.readLine();
        System.out.println(result);

        //(5)关闭
        fis.close();
        input.close();
        socket.close();
    }
}

服务器端的示例代码:

package com.bdit.tcp.fileupload;

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

public class Server {
    
    
    public static void main(String[] args) throws Exception{
    
    
        //服务器在8888端口号监听数据
        @SuppressWarnings("resource")
        ServerSocket server = new ServerSocket(8888);

        while(true){
    
    
            //(2)等待连接 
            //这句代码执行一次,意味着一个客户端连接
            Socket accept = server.accept();

            FileUploadThread ft = new FileUploadThread(accept);
            ft.start();
        }
    }
}

服务器端处理每一个客户端上传文件的线程类代码:

package com.bdit.tcp.fileupload;

import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileUploadThread extends Thread{
    
    
    private Socket socket;
    private String dir = "upload/";

    public FileUploadThread(Socket socket) {
    
    
        super();
        this.socket = socket;
    }

    public void run(){
    
    
        FileOutputStream fos = null;
        try {
    
    
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);

            //读取文件名(含后缀名)
            String filename = dis.readUTF();
            //截取后缀名
            String ext = filename.substring(filename.lastIndexOf("."));
            //生成时间戳
            SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
            filename = sf.format(new Date());
            //用新文件路径构建文件输出流
            fos = new FileOutputStream(dir + filename + ext);
            //接收文件内容
            byte[] data = new byte[1024];
            while(true){
    
    
                int len = is.read(data);
                if(len==-1){
    
    
                    break;
                }
                fos.write(data, 0, len);
            }

            //返回结果
            OutputStream out = socket.getOutputStream();
            PrintStream ps = new PrintStream(out);
            ps.println(filename + ext + ":已上传完毕");
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }finally{
    
    
            try {
    
    
                fos.close();
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

1.5、案例:多个客户端群聊

需求:模拟聊天室群聊

客户端要先登录,登录成功之后才能发送和接收消息

思考分析:

● 服务器端,需要为每个客户端开启一个线程通信,这样才能实现多个客户端“同时”与服务器通信

● 客户端,需要把收消息功能与发消息功能分开两个线程,这样才能“同时收发”,即可以发消息,也可以接收其他客户端的聊天消息

● 服务器端要分别处理客户端的“登录”、“退出”、“聊天”的消息,所以这里设计了Code常量类,用状态值区分“登录”、“退出”、“聊天”

● 这里设计Message类,包含code属性,区别是“登录”、“退出”、“聊天”,username属性表示用户名,表明消息是谁发的,content属性,存储消息内容,如果是登录,就用来存储密码。

● 这里消息是Message对象,因此在客户端与服务器之间传输的是对象,所以选择ObjectOutputStream和ObjectInputStream。

● 这里的Message类与Code类是服务器端和客户端“共享”的,要保持一致。特别注意包名和序列化版本ID。

Message类与Code类的示例代码:

package com.bdit.tcp.chat.bean;

public class Code {
    
    
    public static final int LOGIN = 1;
    public static final int CHAT = 2;
    public static final int LOGOUT = 3;

    public static final int SUCCESS = 1;
    public static final int FAIL = 2;
}
package com.bdit.tcp.chat.bean;

import java.io.Serializable;

public class Message implements Serializable{
    
    
    private static final long serialVersionUID = 1L;
    private int code;
    private String username;
    private String content;
    public Message(int code, String username, String content) {
    
    
        super();
        this.code = code;
        this.username = username;
        this.content = content;
    }

    public Message() {
    
    
        super();
    }

    public int getCode() {
    
    
        return code;
    }
    public void setCode(int code) {
    
    
        this.code = code;
    }
    public String getUsername() {
    
    
        return username;
    }
    public void setUsername(String username) {
    
    
        this.username = username;
    }
    public String getContent() {
    
    
        return content;
    }
    public void setContent(String content) {
    
    
        this.content = content;
    }
}

服务器端用户管理类代码:

package com.bdit.tcp.chat.server;

import java.util.HashMap;

public class UserManager {
    
    
    public static HashMap<String,String> allUsers = new HashMap<String,String>();
    static{
    
    
        allUsers.put("gangge", "123");
        allUsers.put("xiaobai", "456");
        allUsers.put("gujie", "789");
    }

    public static boolean login(String username, String password){
    
    
        if(allUsers.get(username)!=null && allUsers.get(username).equals(password)){
    
    
            return true;
        }else{
    
    
            return false;
        }
    }
}

服务器端示例代码:

package com.bdit.tcp.chat.server;

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

public class Server {
    
    
    public static void main(String[] args)throws Exception {
    
    
        @SuppressWarnings("resource")
        ServerSocket server = new ServerSocket(9999);

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

            ClientHandlerThread ct = new ClientHandlerThread(socket);
            ct.start();
        }
    }
}

服务器端处理消息的线程类代码:

package com.bdit.tcp.chat.server;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import com.bdit.tcp.chat.bean.Code;
import com.bdit.tcp.chat.bean.Message;

public class ClientHandlerThread extends Thread{
    
    
    public static Set<ObjectOutputStream> online = Collections.synchronizedSet(new HashSet<ObjectOutputStream>());

    private Socket socket;
    private String username;
    private ObjectInputStream ois;
    private ObjectOutputStream oos;

    public ClientHandlerThread(Socket socket) {
    
    
        super();
        this.socket = socket;
    }

    public void run(){
    
    
        Message message = null;
        try{
    
    
            ois = new ObjectInputStream(socket.getInputStream());
            oos = new ObjectOutputStream(socket.getOutputStream());

            //接收数据
            while (true) {
    
    
                message = (Message) ois.readObject();

                if(message.getCode() == Code.LOGIN){
    
    
                    //如果是登录,则验证用户名密码
                    username = message.getUsername();
                    String password = message.getContent();
                    if(UserManager.login(username, password)){
    
    
                        message.setCode(Code.SUCCESS);
                        oos.writeObject(message);

                        //并将该用户添加到在线人员名单中
                        online.add(oos);

                        message.setCode(Code.CHAT);
                        message.setContent("上线了");
                        //通知其他人,xx上线了
                        sendToOther(message);
                    }else{
    
    
                        message.setCode(Code.FAIL);
                        oos.writeObject(message);
                    }
                }else if(message.getCode() == Code.CHAT){
    
    
                    //如果是聊天信息,把消息转发给其他在线客户端
                    sendToOther(message);
                }else if(message.getCode() == Code.LOGOUT){
    
    
                    //通知其他人,xx下线了
                    message.setContent("下线了");
                    sendToOther(message);
                    break;
                }
            }
        }catch(Exception  e){
    
    
            //通知其他人,xx掉线了
            if(message!=null && username!=null){
    
    
                message.setCode(Code.LOGOUT);
                message.setContent("掉线了");
                sendToOther(message);
            }
        }finally{
    
    
            //从在线人员中移除并断开当前客户端
            try {
    
    
                online.remove(oos);
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    private void sendToOther(Message message) {
    
    
        ArrayList<ObjectOutputStream> offline = new ArrayList<ObjectOutputStream>();
        for (ObjectOutputStream on : online) {
    
    
            if(!on.equals(oos)){
    
    
                try {
    
    
                    on.writeObject(message);
                } catch (IOException e) {
    
    
                    offline.add(on);
                }
            }
        }

        for (ObjectOutputStream off : offline) {
    
    
            online.remove(off);
        }
    }
}

客户端示例代码:

package com.bdit.tcp.chat.client;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Scanner;

import com.bdit.tcp.chat.bean.Code;
import com.bdit.tcp.chat.bean.Message;

public class Client {
    
    
    public static void main(String[] args) throws Exception{
    
    
        Socket socket = new Socket("192.168.1.107", 9999);

        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

        //先登录
        Scanner scanner = new Scanner(System.in);
        String username;
        while(true){
    
    
            //输入登录信息
            System.out.println("用户名:");
            username = scanner.nextLine();
            System.out.println("密码:");
            String password = scanner.nextLine();

            Message msg = new Message(Code.LOGIN, username, password);
            //发送登录数据
            oos.writeObject(msg);
            // 接收登录结果
            msg = (Message) ois.readObject();
            if(msg.getCode() == Code.SUCCESS){
    
    
                System.out.println("登录成功!");
                break;
            }else if(msg.getCode() == Code.FAIL){
    
    
                System.out.println("用户名或密码错误,登录失败,重新输入");
            }
        }

        //启动收消息和发消息线程
        SendThread s = new SendThread(oos,username);
        ReceiveThread r = new ReceiveThread(ois);
        s.start();
        r.start();

        s.join();//不发了,就结束
        r.setFlag(false);
        r.join();

        scanner.close();
        socket.close();
    }
}

客户端发消息线程类代码:

package com.bdit.tcp.chat.client;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Scanner;

import com.bdit.tcp.chat.bean.Code;
import com.bdit.tcp.chat.bean.Message;

public class SendThread extends Thread{
    
    
    private ObjectOutputStream oos;
    private String username;

    public SendThread(ObjectOutputStream oos,String username) {
    
    
        super();
        this.oos = oos;
        this.username = username;
    }

    public void run(){
    
    
        try {
    
    
            Scanner scanner = new Scanner(System.in);
            while(true){
    
    
                System.out.println("请输入消息内容:");
                String content = scanner.nextLine();
                Message msg;
                if("bye".equals(content)){
    
    
                    msg = new Message(Code.LOGOUT, username, content);
                    oos.writeObject(msg);
                    scanner.close();
                    break;
                }else{
    
    
                    msg = new Message(Code.CHAT, username, content);
                    oos.writeObject(msg);
                }
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

客户端收消息线程类代码:

package com.bdit.tcp.chat.client;

import java.io.ObjectInputStream;

import com.bdit.tcp.chat.bean.Message;

public class ReceiveThread extends Thread{
    
    
    private ObjectInputStream ois;
    private volatile boolean flag = true;

    public ReceiveThread(ObjectInputStream ois) {
    
    
        super();
        this.ois = ois;
    }
    public void run(){
    
    
        try {
    
    
            while(flag){
    
    
                Message msg = (Message) ois.readObject();
                System.out.println(msg.getUsername() + ":" + msg.getContent());
            }
        } catch (Exception e) {
    
    
            System.out.println("请重新登录");
        }
    }
    public void setFlag(boolean flag) {
    
    
        this.flag = flag;
    }

}

以上案例的网络通信程序是基于阻塞式API的,所以服务器必须为每个客户端都提供一条独立线程进行处理,当服务器需要同时处理大量客户端时,这种做法会导致性能下降。如果要开发高性能网络服务器,那么需要使用Java提供的NIO API,可以让服务器使用一个或有限几个线程来同时处理连接到服务器上的所有客户端。

2、基于UDP协议的网络编程

UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。

UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送,至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说,UDP协议是一种不可靠的协议。无连接的好处就是快,省内存空间和流量,因为维护连接需要创建大量的数据结构。UDP会尽最大努力交付数据,但不保证可靠交付,没有TCP的确认机制、重传机制,如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息。

UDP协议是面向数据报文的信息传送服务。UDP在发送端没有缓冲区,对于应用层交付下来的报文在添加了首部之后就直接交付于ip层,不会进行合并,也不会进行拆分,而是一次交付一个完整的报文。比如我们要发送100个字节的报文,我们调用一次send()方法就会发送100字节,接收方也需要用receive()方法一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。

UDP协议没有拥塞控制,所以当网络出现的拥塞不会导致主机发送数据的速率降低。虽然UDP的接收端有缓冲区,但是这个缓冲区只负责接收,并不会保证UDP报文的到达顺序是否和发送的顺序一致。因为网络传输的时候,由于网络拥塞的存在是很大的可能导致先发的报文比后发的报文晚到达。如果此时缓冲区满了,后面到达的报文将直接被丢弃。这个对实时应用来说很重要,比如:视频通话、直播等应用。

因此UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境,数据报大小限制在64K以下。

2.1、基于UDP协议的网络编程

基于UDP协议的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。

DatagramSocket 类的常用方法:

  1. public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。

  2. public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。

  3. public void close()关闭此数据报套接字。

  4. public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。

  5. public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。
    此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。

DatagramPacket类的常用方法:

  1. public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为
    length 的数据包。 length 参数必须小于等于 buf.length。

  2. public DatagramPacket(byte[] buf,int length,InetAddress address,int
    port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。

  3. public int getLength()返回将要发送或接收到的数据的长度。

1、“发送端”

步骤流程:

  1. 建立发送端的DatagramSocket,需要指定本端的端口号

  2. 建立数据包DatagramPacket

    ●数据

    ●接收端的IP地址

    ●接收端的端口号

  3. 调用DatagramSocket的发送方法

  4. 关闭DatagramSocket

示例代码:

package com.bdit.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;

public class Send {
    
    

    public static void main(String[] args)throws Exception {
    
    
//    1、建立发送端的DatagramSocket
        DatagramSocket ds = new DatagramSocket();

        //要发送的数据
        ArrayList<String> all = new ArrayList<String>();
        all.add("一二三四");
        all.add("五六七八");
        all.add("好好学习");
        all.add("天天向上");

        //接收方的IP地址
        InetAddress ip = InetAddress.getByName("127.0.0.1");
        //接收方的监听端口号
        int port = 9999;
        //发送多个数据报
        for (int i = 0; i < all.size(); i++) {
    
    
//       2、建立数据包DatagramPacket
            byte[] data = all.get(i).getBytes();
            DatagramPacket dp = new DatagramPacket(data, data.length, ip, port);
//       3、调用Socket的发送方法
            ds.send(dp);
        }

//    4、关闭Socket
        ds.close();
    }
}

2、“接收端”

步骤流程:

  1. 建立接收端的DatagramSocket,需要指定本端的IP地址和端口号

  2. 建立数据包DatagramPacket

    需要指定装数据的数组

  3. 调用Socket的接收方法

  4. 拆封数据

  5. 关闭Socket

示例代码:

package com.bdit.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Receive {
    
    

    public static void main(String[] args) throws Exception {
    
    
//    1、建立接收端的DatagramSocket,需要指定本端的监听端口号
        DatagramSocket ds = new DatagramSocket(9999);

        //一直监听数据
        while(true){
    
    
            //    2、建立数据包DatagramPacket
            byte[] buffer = new byte[1024*64];
            DatagramPacket dp = new DatagramPacket(buffer , buffer.length);

            //    3、调用Socket的接收方法
            ds.receive(dp);

            //4、拆封数据
            String str = new String(buffer,0,dp.getLength());
            System.out.println(str);
        }
    }
}

2.2、使用MulticastSocket实现多点广播

Datagram只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播方式发送到数量不等的多个客户端。

IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255。

在这里插入图片描述

MulticastSocket常用的方法:

● MulticastSocket(int port) :创建多播套接字并将其绑定到特定端口。创建一个MulticastSocket对象后,还需要将该MulticastSocket加入到指定的多点广播地址,如果结束也需要脱离多点广播地址。

● void joinGroup(InetAddress mcastaddr) :加入多播组。

● void leaveGroup(InetAddress mcastaddr) :离开多播组。

● void setLoopbackMode(boolean disable) :启用/禁用多播数据报的本地回送。true 表示禁用 LoopbackMode。

案例:群聊

package com.bdit.udp.multi;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Scanner;

public class Chat {
    
    
    private volatile static boolean exit = false;
    private static Scanner input = new Scanner(System.in);
    private static String username;

    public static void main(String[] args) throws IOException {
    
    
        MulticastSocket socket = new MulticastSocket(9999);
        InetAddress ip = InetAddress.getByName("230.0.0.1");
        socket.joinGroup(ip);
        socket.setLoopbackMode(false);

        System.out.print("请输入用户名:");
        username = input.nextLine();

        SendThread s = new SendThread(socket,ip);
        ReceiveThread r = new ReceiveThread(socket);

        s.start();
        r.start();

        try {
    
    
            s.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        socket.close();
        input.close();
    }
    static class SendThread extends Thread{
    
    
        private MulticastSocket socket;
        private InetAddress ip;

        public SendThread(MulticastSocket socket,InetAddress ip) {
    
    
            super();
            this.socket = socket;
            this.ip = ip;
        }
        public void run(){
    
    
            try {
    
    
                while (!exit) {
    
    
                    System.out.print("输入广播消息:");
                    String message = input.nextLine();
                    if ("bye".equals(message)) {
    
    
                        exit = true;
                        break;
                    }

                    byte[] data = (username+":"+message).getBytes();
                    DatagramPacket dp = new DatagramPacket(data, data.length, ip, 9999);

                    socket.send(dp);
                }
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

    }
    static class ReceiveThread extends Thread{
    
    
        private MulticastSocket socket;

        public ReceiveThread(MulticastSocket socket) {
    
    
            super();
            this.socket = socket;
        }

        public void run(){
    
    
            try {
    
    
                while (!exit) {
    
    
                    byte[] data = new byte[1024];
                    DatagramPacket dp = new DatagramPacket(data, data.length);
                    socket.receive(dp);
                    String str = new String(data, 0, dp.getLength());
                    System.out.println(str);
                }
            } catch (IOException e) {
    
    
                exit = false;
            }
        }

    }
}

猜你喜欢

转载自blog.csdn.net/vlucky_long/article/details/108805240