Java 网络编程

目录

一、网络编程

二、Socket 套接字

三、UDP 数据报套接字编程

四、TCP 流套接字编程

总结


一、网络编程

网络编程,指网络主机通过不同的进程,以编程的方式实现网络通信

【发送端和接收端】

在一次网络数据传输时:

发送端:数据的发送方进程发送端主机即网络通信中的源主机

接收端:数据的接收方进程接收端主机即网络通信中的目的主机

收发端:发送端和接收端两端,简称为收发端。

注:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。


【客户端和服务端】

服务端:提供服务的进程。

客户端:获取服务的进程。

注:客户端获取服务资源,客户端保存资源在服务端。


【请求和响应】

获取一个网络资源,涉及到两次网络数据传输:① 请求数据的发送、② 响应数据的发送。

请求 (request):客户端给服务器发送的数据。

响应 (response):服务器给客户端返回的数据。


二、Socket 套接字

Socket 套接字,系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元

基于 Socket 套接字的网络程序开发就是网络编程。

【分类】

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

1、数据报套接字:使用传输层 UDP 协议

UDP,即 User Datagram Protocol (用户数据报协议),传输层协议。

【UDP 特点】

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 全双工
  • 有接收缓冲区,无发送缓冲区
  • 大小受限:一次最多传输 64k

对于数据报来说,数据传输是一块一块的,发送一块数据假如 100 个字节,必须一次发送,接收也必须一次接收 100 个字节,而不能分 100 次,每次接收 1 个字节。


2、流套接字:使用传输层 TCP 协议

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

【TCP 特点】

  • 有连接
  • 可靠传输
  • 面向字节流
  • 全双工
  • 有接收缓冲区,也有发送缓冲区
  • 大小不限

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


3、原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据。


三、UDP 数据报套接字编程

对于 UDP 协议来说,具有无连接、面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。

Java 中使用 UDP协议 通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 类作为发送或接收的 UDP 数据报

1、DatagramSocket

DatagrmSocket 是 UDP Socket 用于发送和接收 UDP 数据报

【构造方法】

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

【方法】

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

2、DatagramPacket

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

【构造方法】

构造方法说明
DatagramPacket(byte[] buf, int length)构造一个 DatagramPacket 用于接收数据报,接收的数据保存在字节数组 (参数 buf) 中,接收指定长度 (参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个 DatagramPacket 用于发送数据报,发送的数据保存在字节数组 (参数 buf) 中,从 0 到指定长度 (参数 length),指定目的主机的 IP 和端口号 (参数 address)

【方法】

方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址
int getPort()从接收的数据报中,获取发送端主机端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据
int getLength()获取数据报的有效长度

3、InetSocketAddress

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

【构造方法】

构造方法说明

InetSocketAddress(InetSocketAddress addr, int port)

创建一个 Socket 地址,包含 IP 地址和端口号

【UDP编程案例】

通过UDP服务器与UDP客户端的交互,实现客户端输入请求,服务器返回响应的功能。

【UDP回显服务器】

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

public class UdpEchoServer {

    //1、创建 DatagramSocket 对象
    private DatagramSocket socket;

    //一个端口只能被一个进程绑定,一个进程可以绑定多个端口
    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); //接收数据报
            //将读到的字节数组转成 String,方便后续的逻辑处理
            //从字节数组的下标 0 位置开始构造 String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            //2、根据请求构造响应
            String response = process(request);

            //3、把响应返回给客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 
                    response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(responsePacket);

            //打印日志
            System.out.printf("[%s:%d], request: %s, response: %s", requestPacket.getAddress().toString(), 
                    requestPacket.getPort(), request, response);
        }
    }

    //根据需求编写 process 方法
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        //给客户端提供端口号
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

【UDP回显客户端】

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

public class UdpEchoClient {

    private DatagramSocket socket;

    private String serverIp;
    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 {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("->");
            
            //1、从控制台读取要发送的请求数据
            if (!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();

            //2、构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);

            socket.send(requestPacket);
            //3、读取服务器的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);

            //4、把响应显示在控制台上
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        //通过 IP 地址和端口号找到服务器
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

【总结】 

1、服务器启动后,在 receive 处阻塞,等待客户端 send 请求。

2、客户端在 scanner.hasNext 处等待控制台输入,控制台输入后,构造请求并 send,然后在 receive 处阻塞,等待响应。

3、服务器 receive 请求后,进行 process 处理,构造响应并 send,然后进行下一次循环,继续在 receive 处阻塞等待下一次请求。

4、客户端 receive 响应并打印在控制台后,进行下一次循环,等待控制台输入。

【构造 DatagramPacket 对象】

1、构造时指定空白字节数组。(搭配 receive 使用)

    DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

2、构造时指定有内容的数组,并指定 IP 和端口。(搭配 send 使用)

    DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
            response.getBytes().length, requestPacket.getSocketAddress());

3、构造时指定有内容的数组,并分开指定 IP 和端口。(搭配 send 使用)

    DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 
            request.getBytes().length,InetAddress.getByName(serverIp), serverPort);

【简易英译汉词典】

继承 UdpEchoServer 类,重写 process 方法,实现简易的英译汉需求。

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

public class UdpDictServer extends UdpEchoServer{
    //构造一个哈希表
    private HashMap<String, String> hashMap = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);
        //设置键值对 (实际词典就是一个巨大的 hashMap)
        hashMap.put("moon", "月亮");
        //后续可以添加无数个英汉键值对
    }

    @Override
    public String process(String request) {
        return hashMap.getOrDefault(request, "词典中没有找到该单词");
    }

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

四、TCP 流套接字编程

1、ServerSocket

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

【构造方法】

构造方法说明
ServerSocket(int port)创建一个服务器流套接字 Socket,并绑定到指定端口

【方法】

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

2、Socket 

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

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

【构造方法】

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

【方法】

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

【TCP编程案例】

通过TCP服务器与TCP客户端的交互,实现客户端输入请求,服务器返回响应的功能。

【TCP回显服务器】

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;

public class TcpEchoServer {
    private ServerSocket serverSocket;

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

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            //接收客户端发起的连接请求
            Socket clientSocket = serverSocket.accept();
            //处理连接
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
        //循环读取客户端的请求并返回响应
        try {
            InputStream inputStream = clientSocket.getInputStream(); //读取
            OutputStream outputStream = clientSocket.getOutputStream(); //写入
            Scanner scanner = new Scanner(inputStream); //包装输入流
            PrintWriter printWriter = new PrintWriter(outputStream); //包装输出流

            while (true) {
                //scanner 读取网络请求
                if (!scanner.hasNext()) {
                    //客户端断开连接,读取结束
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }

                //1、获取请求
                //next 读到空白符才会结束,因此客户端的请求必须带有 \n 或空格
                String request = scanner.next();

                //2、构造响应
                String response = processResponse(request);

                //3、返回响应给客户端
                printWriter.println(response);
                //刷新 OutputStream 的缓冲区
                printWriter.flush();

                System.out.printf("[%s:%d], request: %s, response: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //释放 clientSocket,由于每个客户端都有一个 clientSocket,
            //若不释放,客户端越来越多,将会把文件描述符表占满
            try {
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private String processResponse(String request) {
        return request;
    }

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

【TCP回显客户端】

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;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        //TCP 是有连接的,故 TCP socket 会保存对端的信息
        socket = new Socket(serverIp, serverPort); //和对应的服务器建立连接 (系统内核中完成)
    }

    public void start() {
        System.out.println("启动客户端");

        try {
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                System.out.print("->");

                //1、从控制台读取请求
                if (!scannerConsole.hasNext()) {
                    break;
                }
                String request = scannerConsole.next();

                //2、将请求发给服务器
                //使用 PrintWriter 方便请求末尾带有 \n
                printWriter.println(request);
                //刷新 OutputStream 的缓冲区
                printWriter.flush();

                //3、从服务器读取响应
                String response = scannerNetwork.next();

                //4、打印响应至控制台
                System.out.println(response);

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

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

【总结】 

1、服务端在 accept 处阻塞等待客户端建立连接。

2、构造客户端对象的 new 操作触发了客户端与服务端建立连接请求,即"三次握手"

3、服务器 accept 客户端连接后,进入 processConnection 方法处理连接,在 hasNext 处阻塞等待客户端请求。

4、客户端在 hasNext 处等待控制台输入,控制台输入后,构造请求并写入服务器,然后等待服务器响应。

5、服务器读取到请求后,构造响应并写回客户端。

6、客户端读取响应,并打印至控制台。 


【如何运行多个实例】

打开第 ① 步中 Edit Configurations,第 ② 步找到要设置的类,点击 Modify options,第 ③ 步将 Allow multiple instances 勾上。

【解决TCP回显服务器不能支持多个客户端同时访问的问题】 

在上述TCP回显服务器代码中,有一个明显的缺陷,即第一个客户端工作时,第二个客户端没有任何响应;当第一个客户端断开连接,第二个客户端才能正常工作。此时我们就可以引入线程池来解决该问题,每次来一个客户端,就用应该新线程来执行。

    public void start() throws IOException {
        System.out.println("启动服务器");
        //创建线程池实例
        ExecutorService pool = Executors.newCachedThreadPool();

        while (true) {
            Socket clientSocket = serverSocket.accept();
            //给线程池添加任务
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

总结

1、网络编程,指网络主机通过不同的进程,以编程的方式实现网络通信。

2、DatagrmSocket 用于发送和接收 UDP 数据报,DatagramPacket 是 UDP 数据报。

3、网络连接本质上就是通信双方各自保存对方的信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值