Java 网络编程技术总结


Java 有关 UDP 和 TCP 两种协议的网络编程技术,在大部分情况下,很少会使用到,但是偶尔也会使用。对于大部分开发人员来说,最常遇到的使用场景有两种:一种场景是公司的产品或项目需要跟相关的硬件进行对接,另一种场景就是需要跟其它公司进行接口对接(比如某些银行提供的接口就要求使用 socket 对接),所以我们还是得要简单学习和了解一下 Java 的网络编程技术。

对于以上两种常见的使用场景来说,我们使用 Java 网络编程技术去解决,还是很容易的,比较容易上手。
本篇博客的主要目的在于对 Java 网络编程进行一个简单的总结,以便后续需要用到的时候,能够快速找到。


一、UDP 通信

UDP 协议是一种只管传输,不需要回应的网络协议,它在通信的两端各建立一个 Socket 对象,这俩 Socket 对象都可以发送数据和接收数据,但是不需要对方的回复,因此传输效率很高,对于基于 UDP 协议的通信双方而言,其实没有所谓的客户端和服务器的概念。Java 提供以下方法来实现 UDP 协议的通信:

方法名 说明
DatagramSocket() 创建 UDP Socket 对象
DatagramPacket(byte[] buf,int len,InetAddress add,int port) 创建数据包,指定目标主机端口
void send(DatagramPacket p) 发送数据报包
void close() 关闭连接
void receive(DatagramPacket p) 接收数据报包

发送端代码演示:
import java.io.*;
import java.net.*;

//使用控制台,循环发送字符串数据,如果发送的是字符串 exit ,双方服务都停止
public class ClientDemo {
    public static void main(String[] args) {

        //使用 BufferedReader 来作为控制台的字符串输入交互
        //使用 DatagramSocket 创建 UDP 的 Socket 对象
        try (BufferedReader bw = new BufferedReader(new InputStreamReader(System.in));
             DatagramSocket ds = new DatagramSocket()) {

            while (true) {
                String str = bw.readLine();
                byte[] bytes = str.getBytes();
                //准备给本机的 8888 端口,发送 UDP 数据,
                InetAddress address = InetAddress.getLocalHost();
                int port = 8888;
                //对于 UDP 协议来说,使用 DatagramPacket 来封装要发送的数据包
                DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
                ds.send(dp);

                //如果发送的是 exit 字符串,就停止发送服务
                if ("exit".equals(str)) {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接收端代码演示:

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

//循环接收发送来的数据,当接收到 exit 字符串时,停止服务不再接收数据
public class ServerDemo {
    public static void main(String[] args) {

        //使用 DatagramSocket 创建 UDP 的 Socket 对象
        try (DatagramSocket ds = new DatagramSocket(8888);) {

            while (true) {
                //使用 DatagramPacket 封装接收的 UDP 数据包
                DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
                ds.receive(dp);
                String result = new String(dp.getData(), 0, dp.getLength());
                System.out.println(result);

                //如果接收到 exit 字符串,则停止服务,不再接收
                if ("exit".equals(result)) {
                    break;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上代码就是 Java 采用 UDP 协议进行通信的基本编码,大家可以根据工作中的实际需要进行改进,总体来说还算比较简单。


二、TCP 通信

TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过三次握手。

TCP 协议是我们最常使用的通信协议,对于客户端需要创建 Socket 对象,对于服务端需要创建 ServerSocket 对象。

无论对于客户端还是服务端,可以通过其 Scoket 对象获取到 InputStream 流和 OutputStream 流。通过 OutputStream 流写数据就是发送数据,通过 InputStream 流读数据就是接收数据。对于客户端 Socket 对象发送数据来说,需要特别注意:如果客户端 Socket 对象后续不再发送数据的话,需要调用其 shutdownOutput 方法明确告诉服务后续不再发送数据,这样服务端才会停止接收数据,否则服务端接收数据的代码将会阻塞,无法继续向下运行。下面我们还是通过代码来演示一下吧。

客户端代码演示:

import java.io.*;
import java.net.Socket;

//本代码演示客户端通过 TCP 协议,向服务端上传图片文件
public class ClientDemo {
    public static void main(String[] args)  {

        //创建客户端 Socket 对象,创建字节缓冲流对象读取要上传的图片文件
        try (Socket socket = new Socket("127.0.0.1", 8888);
             BufferedInputStream bis = new BufferedInputStream(
                     new FileInputStream("d:\\数据产品组大合照.jpg"));) {

            //获取 Socket 输出流对象,用于向服务端发送数据,
            //由于该输出流是从 Socket 中获取,因此不需要主动关闭,
            //因为 Socket 关闭后,该输出流会自动关闭
            BufferedOutputStream bos = 
                new BufferedOutputStream(socket.getOutputStream());

            byte[] bArr = new byte[1024];
            int len;
            while ((len = bis.read(bArr)) != -1) {
                bos.write(bArr, 0, len);
            }
            bos.flush();

            //注意:此行代码非常重要,告诉服务端:这边的客户端数据已经全部发送完毕了
            socket.shutdownOutput();

            //接收服务端返回来的消息,打印到控制台上,
            //由于该输入流是从 Socket 中获取,因此不需要主动关闭,
            //因为 Socket 关闭后,该输入流会自动关闭
            BufferedReader br = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服务端的代码演示:

import java.io.*;
import java.net.*;

//接收客户端发送来的图片数据,并保存到硬盘中
public class ServerDemo {
    public static void main(String[] args) {

        //创建服务端 ServerSocket 对象,创建字节流缓冲对象把接收的图片保存到硬盘
        try (ServerSocket ss = new ServerSocket(8888);
             BufferedOutputStream bos = new BufferedOutputStream(
                     new FileOutputStream("d:\\copy.jpg"));
             //获取客户端的 socket 对象
             //注意这里获取的客户端 Socket 对象由于在 try 小括号内初始化,
             //因此出了 try 代码块后,会自动关闭客户端 Socket 对象
             //这样客户端那边就可以结束了
             Socket accept = ss.accept()) {

            //接收客户端发来的数据,保存到硬盘中
            //由于该输入流是从 Socket 中获取,因此不需要主动关闭,
            //因为 Socket 关闭后,该输入流会自动关闭
            BufferedInputStream bis = 
                new BufferedInputStream(accept.getInputStream());

            byte[] bArr = new byte[1024];
            int len;
            while ((len = bis.read(bArr)) != -1) {
                bos.write(bArr, 0, len);
            }

            //获取 Socket 输出流对象,用于向客户端发送数据,
            //由于该输出流是从 Socket 中获取,因此不需要主动关闭,
            //因为 Socket 关闭后,该输出流会自动关闭
            BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(accept.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面的服务端代码,只能接收到一个客户端的文件上传,为了能够支持多客户端,我们可以采用线程池进行改进,首先我们先写一个接收客户端上传的图片数据,并保存到硬盘的线程类,代码如下所示:

import java.io.*;
import java.net.Socket;
import java.util.UUID;

public class ThreadSocket implements Runnable {

    private Socket acceptSocket;

    public ThreadSocket(Socket accept) { this.acceptSocket = accept; }

    @Override
    public void run() {
        //创建字节缓冲流对象,用于向硬盘保存图片数据
        try (BufferedOutputStream bos = new BufferedOutputStream(
                     new FileOutputStream("d:\\" + UUID.randomUUID() + ".jpg"))) {

            //接收客户端发送过来的数据
            BufferedInputStream bis = 
                new BufferedInputStream(acceptSocket.getInputStream());

            byte[] bArr = new byte[1024];
            int len;
            while ((len = bis.read(bArr)) != -1) {
                bos.write(bArr, 0, len);
            }

            //向客户端发送消息
            BufferedWriter bw = new BufferedWriter(
                    new OutputStreamWriter(acceptSocket.getOutputStream()));
            bw.write("图片已经上传成功");
            bw.newLine();
            bw.flush();

            //关闭服务器接收到的客户端 socket 对象,这个很重要,不然的话,客户端无法结束
            acceptSocket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后我们使用线程池,改进服务端代码,这样就可以同时接纳多个客户端上传图片了,代码如下所示:

import java.net.*;
import java.util.concurrent.*;

public class ServerDemo {
    public static void main(String[] args) {

        try (ServerSocket ss = new ServerSocket(8888)) {
            //创建线程池
            ThreadPoolExecutor pool = new ThreadPoolExecutor(
                    5,//核心线程数量
                    10,//线程池的总数量
                    20,//临时线程空闲时间
                    TimeUnit.SECONDS,//临时线程空闲时间的单位
                    new ArrayBlockingQueue<>(5),//阻塞队列
                    Executors.defaultThreadFactory(),//创建线程的方式
                    new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
            );

            while (true) {
                Socket acceptSocket = ss.accept();
                ThreadSocket ts = new ThreadSocket(acceptSocket);
                //向线程池提交任务
                pool.submit(ts);
            }

            //pool.shutdown(); //关闭线程池
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上就是简单的介绍了 Java 有关 UDP 和 TCP 网络编程的相关技术,其中 UDP 示例是发送文字信息,TCP 示例是上传图片文件,并采用线程池进行了改进。

有关 TCP 发送和接收文字信息,由于非常简单,所以这里就不介绍了。之所以介绍 TCP 上传图片文件,是因为让大家注意客户端 Scoket 对象发送完数据后,要调用 shutdownOutput 告诉服务端,否则服务端是不知道客户端已经发送完毕了。

希望以上代码示例,大家能够看懂,对大家有所帮助。



相关