Java核心API-网络编程

网络编程

网络应用模型

  • C/S:这里的C是指Client,即客户端,而S是指Server,即服务端,网络上的的应用本质上就是两台计算机上的软件进行交互,而客户端和服务端就是对应的两个应用程序,即客户端应用程序和服务端应用程序
  • B/S:这里的B是Browser,即浏览器,而S是指Server,浏览器是一个通用的客户端,可以与不同的服务端进行交互,但是本质上B/S还是C/S结构,只不过浏览器是一个通用的客户端而已

可靠传输与不可靠传输

TCP协议与UDP协议都是传输协议,客户端程序与服务端程序基于这些协议完成网络间的数据交互

  • TCP是可靠传输协议,是面向连接的协议,保证数据传输中的可靠性和完整性
    TCP保证可靠传输,但是传输效率低,占用带宽高
  • UDP是不可靠传输协议,不保证数据传输的完整性
    UDP不保证可靠传输,但是传输速度块,占用带宽小

Socket与ServerSocket

  • java.net.Socket
    Socket(套接字)封装了TCP协议的通讯细节,我们使用它可以与服务端建立网络链接,并通过它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
  • java.net.ServerSocket
    • ServerSocket运行在服务端,作用有两个:
      • 向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的
      • 监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互
        在这里插入图片描述

与服务端建立连接案例(聊天室)

客户端:

package socket;

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

/**
 * 聊天室客户端
 */
public class Client {
    /*
     * java.net.Socket 套接字
     * Socket 封装了 TCP 协议的通讯细节,我们通过它可以与远端计算机建立链接,
     * 并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
     * 与远端计算机的数据交互工作。
     * 我们可以把 Socket 想象成是一个电话,电话有一个听筒(输入流),一个麦克
     * 风(输出流),通过它们就可以与对方交流了。
     */
    private Socket socket;

    /**
     * 构造方法,用来初始化客户端
     */
    public Client() {
        try {
            System.out.println("正在链接服务端...");
            /*
             * 实例化 Socket 时要传入两个参数
             * 参数 1:服务端的地址信息
             * 可以是 IP 地址,如果链接本机可以写"localhost"
             * 参数 2:服务端开启的服务端口
             * 我们通过 IP 找到网络上的服务端计算机,通过端口链接运行在该机器上
             * 的服务端应用程序。
             * 实例化的过程就是链接的过程,如果链接失败会抛出异常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端建立链接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

服务端:

package socket;

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

/**
 * 聊天室服务端
 */
public class Server {
    /**
     * 运行在服务端的 ServerSocket 主要完成两个工作:
     * 1: 向服务端操作系统申请服务端口,客户端就是通过这个端口与 ServerSocket 建立链接
     * 2: 监听端口,一旦一个客户端建立链接,会立即返回一个 Socket。通过这个 Socket
     *    就可以和该客户端交互了
     * 
     * 我们可以把 ServerSocket 想象成某客服的"总机"。用户打电话到总机,总机分配一个
     * 电话使得服务端与你沟通。
     */
    private ServerSocket serverSocket;

    /**
     * 服务端构造方法,用来初始化
     */
    public Server() {
        try {
            System.out.println("正在启动服务端...");
            /*
             * 实例化 ServerSocket 时要指定服务端口,该端口不能与操作系统其他
             * 应用程序占用的端口相同,否则会抛出异常:
             * java.net.BindException: address already in use
             * 端口是一个数字,取值范围: 0 - 65535 之间。
             * 6000 之前的端口不要使用,密集绑定系统应用和流行应用程序。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            System.out.println("等待客户端链接...");
            /*
             * ServerSocket 提供了接受客户端链接的方法:
             * Socket accept()
             * 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
             * 的链接,直到一个客户端链接,此时该方法会立即返回一个 Socket 实例
             * 通过这个 Socket 就可以与客户端进行交互了。
             * 可以理解为此操作是接电话,电话没响时就一直等。
             */
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端链接了!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

客户端与服务端完成第一次通讯(发送一行字符串)

Socket提供了两个重要的方法:

  • OutputStream getOutputStream()
    该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方
  • InputStream getInputStream()
    通过该方法获取的字节输入流读取的是远端计算机发送过来的数据

客户端:

package socket;

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

/**
 * 聊天室客户端
 */
public class Client {
    /*
     * java.net.Socket 套接字
     * Socket 封装了 TCP 协议的通讯细节,我们通过它可以与远端计算机建立链接,
     * 并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
     * 与远端计算机的数据交互工作。
     * 我们可以把 Socket 想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),
     * 通过它们就可以与对方交流了。
     */
    private Socket socket;

    /**
     * 构造方法,用来初始化客户端
     */
    public Client() {
        try {
            System.out.println("正在链接服务端...");
            /*
             * 实例化 Socket 时要传入两个参数
             * 参数 1: 服务端的地址信息
             * 可以是 IP 地址,如果链接本机可以写 "localhost"
             * 参数 2: 服务端开启的服务端口
             * 我们通过 IP 找到网络上的服务端计算机,通过端口链接运行在该机器上
             * 的服务端应用程序。
             * 实例化的过程就是链接的过程,如果链接失败会抛出异常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端建立链接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            /*
             * Socket 提供了一个方法:
             * OutputStream getOutputStream()
             * 该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
             */
            // 低级流,将字节通过网络发送给对方
            OutputStream out = socket.getOutputStream();
            String line = "你好服务端!";
            byte[] data = line.getBytes(StandardCharsets.UTF_8);
            out.write(data.length); // 单独发送一个字节,表示后面字符串转换的字节数组的长度
            out.write(data); // 然后再将字符串转换的所有字节发送给服务端
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

服务端:

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 聊天室服务端
 */
public class Server {

    /**
     * 运行在服务端的 ServerSocket 主要完成两个工作:
     * 1: 向服务端操作系统申请服务端口,客户端就是通过这个端口与 ServerSocket 建立链接
     * 2: 监听端口,一旦一个客户端建立链接,会立即返回一个 Socket。通过这个 Socket
     *    就可以和该客户端交互了
     *
     * 我们可以把 ServerSocket 想象成某客服的"总机"。用户打电话到总机,总机分配一个
     * 电话使得服务端与你沟通。
     */
    private ServerSocket serverSocket;

    /**
     * 服务端构造方法,用来初始化
     */
    public Server() {
        try {
            System.out.println("正在启动服务端...");
            /*
             * 实例化 ServerSocket 时要指定服务端口,该端口不能与操作系统其他
             * 应用程序占用的端口相同,否则会抛出异常:
             * java.net.BindException: address already in use
             * 端口是一个数字,取值范围: 0 - 65535 之间。
             * 6000 之前的端口不要使用,密集绑定系统应用和流行应用程序。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            System.out.println("等待客户端链接...");
            /*
             * ServerSocket 提供了接受客户端链接的方法:
             * Socket accept()
             * 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
             * 的链接,直到一个客户端链接,此时该方法会立即返回一个 Socket 实例
             * 通过这个 Socket 就可以与客户端进行交互了。
             * 可以理解为此操作是接电话,电话没响时就一直等。
             */
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端链接了!");

            // 需求:读取客户端发送过来的一个字符串
            /*
             * Socket 提供的一个重要方法:
             * InputStream getInputStream()
             * 实现客户端循环发消息给服务端
             * 通过 Socket 获取一个字节输入流,读取的是来自对方送过来的字节数据
             */
            InputStream in = socket.getInputStream();

            // 首先单独读取 1 个字节,接收客户端发送过来的数据的长度
            int len = in.read();
            byte[] data = new byte[len];
            in.read(data);
            String line = new String(data, StandardCharsets.UTF_8);
            System.out.println("客户端说:" + line);

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

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

实现客户端循环发消息给服务端

客户端:

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端
 */
public class Client {

    /*
     * java.net.Socket 套接字
     * Socket 封装了 TCP 协议的通讯细节,我们通过它可以与远端计算机建立链接,
     * 并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
     * 与远端计算机的数据交互工作。
     * 我们可以把 Socket 想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),
     * 通过它们就可以与对方交流了。
     */
    private Socket socket;

    /**
     * 构造方法,用来初始化客户端
     */
    public Client() {
        try {
            System.out.println("正在链接服务端...");
            /*
             * 实例化 Socket 时要传入两个参数
             * 参数 1: 服务端的地址信息
             * 可以是 IP 地址,如果链接本机可以写 "localhost"
             * 参数 2: 服务端开启的服务端口
             * 我们通过 IP 找到网络上的服务端计算机,通过端口链接运行在该机器上
             * 的服务端应用程序。
             * 实例化的过程就是链接的过程,如果链接失败会抛出异常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端建立链接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            /*
             * Socket 提供了一个方法:
             * OutputStream getOutputStream()
             * 该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
             */
            // 低级流,将字节通过网络发送给对方
            OutputStream out = socket.getOutputStream();

            // 实现:循环操作,将控制台输入的每一行字符串陆续发送给服务端
            Scanner scanner = new Scanner(System.in);
            System.out.println("开始聊天吧!");
            while (true) {
                String line = scanner.nextLine();
                byte[] data = line.getBytes(StandardCharsets.UTF_8);
                out.write(data.length); // 单独发送一个字节,表示后面字符串转换的字节数组的长度
                out.write(data); // 然后再将字符串转换的所有字节发送给服务端
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                /*
                 * 通讯完毕后调用 socket 的 close 方法。
                 * 该方法会给对方发送断开信号。
                 */
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

服务端:

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 聊天室服务端
 */
public class Server {

    /**
     * 运行在服务端的 ServerSocket 主要完成两个工作:
     * 1: 向服务端操作系统申请服务端口,客户端就是通过这个端口与 ServerSocket 建立链接
     * 2: 监听端口,一旦一个客户端建立链接,会立即返回一个 Socket。通过这个 Socket
     *    就可以和该客户端交互了
     *
     * 我们可以把 ServerSocket 想象成某客服的"总机"。用户打电话到总机,总机分配一个
     * 电话使得服务端与你沟通。
     */
    private ServerSocket serverSocket;

    /**
     * 服务端构造方法,用来初始化
     */
    public Server() {
        try {
            System.out.println("正在启动服务端...");
            /*
             * 实例化 ServerSocket 时要指定服务端口,该端口不能与操作系统其他
             * 应用程序占用的端口相同,否则会抛出异常:
             * java.net.BindException: address already in use
             * 端口是一个数字,取值范围: 0 - 65535 之间。
             * 6000 之前的端口不要使用,密集绑定系统应用和流行应用程序。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端开始工作的方法
     * 需要注意的几个点:
     * 当客户端不再与服务端通讯时,需要调用 socket.close() 断开链接,此时会发送断开链接的信号给服务端。这
     * 时服务端的 br.readLine() 方法会返回 null,表示客户端断开了链接。
     * 当客户端链接后不输入信息发送给服务端时,服务端的 br.readLine() 方法是出于阻塞状态的,直到读取了一行
     * 来自客户端发送的字符串。
     */
    public void start() {
        try {
            System.out.println("等待客户端链接...");
            /*
             * ServerSocket 提供了接受客户端链接的方法:
             * Socket accept()
             * 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
             * 的链接,直到一个客户端链接,此时该方法会立即返回一个 Socket 实例
             * 通过这个 Socket 就可以与客户端进行交互了。
             * 可以理解为此操作是接电话,电话没响时就一直等。
             */
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端链接了!");

            /*
             * Socket 提供的方法:
             * InputStream getInputStream()
             * 获取的字节输入流读取的是对方计算机发送过来的字节
             */
            InputStream in = socket.getInputStream();
            int len;
            /*
             * 首先单独读取 1 个字节,接收客户端发送过来的数据的长度
             * 如果读取一个字节返回值为 -1,则说明流读取到了末尾,对于这里来讲就是客户端
             * 与我们断开连接,我们才无法再从流中读取字节
             */
            while ((len = in.read()) != -1) {
                byte[] data = new byte[len];
                in.read(data);
                String line = new String(data, StandardCharsets.UTF_8);
                System.out.println("客户端说:" + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

需要注意的几个点:

  • 当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端,这时服务端的br.readLine()方法会返回null,表示客户端断开了连接
  • 当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是处于阻塞状态的,直到读取了一行来自客户端发送的字符串

TCP进行连接时的三次握手

在这里插入图片描述

TCP断开连接时的四次挥手

在这里插入图片描述

多客户端链接

只有第一个连接的客户端可以与服务端说话

原因:
服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了连接并返回了Socket,此时可以与其交互,而第二个客户端建立连接时,由于服务端没有再次调用accept,因此无法与其交互

服务器端:

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 聊天室服务端
 */
public class Server {

    /**
     * 运行在服务端的 ServerSocket 主要完成两个工作:
     * 1: 向服务端操作系统申请服务端口,客户端就是通过这个端口与 ServerSocket 建立链接
     * 2: 监听端口,一旦一个客户端建立链接,会立即返回一个 Socket。通过这个 Socket
     *    就可以和该客户端交互了
     *
     * 我们可以把 ServerSocket 想象成某客服的"总机"。用户打电话到总机,总机分配一个
     * 电话使得服务端与你沟通。
     */
    private ServerSocket serverSocket;

    /**
     * 服务端构造方法,用来初始化
     */
    public Server() {
        try {
            System.out.println("正在启动服务端...");
            /*
             * 实例化 ServerSocket 时要指定服务端口,该端口不能与操作系统其他
             * 应用程序占用的端口相同,否则会抛出异常:
             * java.net.BindException: address already in use
             * 端口是一个数字,取值范围: 0 - 65535 之间。
             * 6000 之前的端口不要使用,密集绑定系统应用和流行应用程序。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端链接...");
                /*
                 * ServerSocket 提供了接受客户端链接的方法:
                 * Socket accept()
                 * 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
                 * 的链接,直到一个客户端链接,此时该方法会立即返回一个 Socket 实例
                 * 通过这个 Socket 就可以与客户端进行交互了。
                 * 可以理解为此操作是接电话,电话没响时就一直等。
                 */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端链接了!");

                /*
                 * Socket 提供的方法:
                 * InputStream getInputStream()
                 * 获取的字节输入流读取的是对方计算机发送过来的字节
                 */
                InputStream in = socket.getInputStream();
                int len;
                /*
                 * 首先单独读取 1 个字节,接收客户端发送过来的数据的长度
                 * 如果读取一个字节返回值为 -1,则说明流读取到了末尾,对于这里来讲就是客户端
                 * 与我们断开连接,我们才无法再从流中读取字节
                 */
                while ((len = in.read()) != -1) {
                    byte[] data = new byte[len];
                    in.read(data);
                    String line = new String(data, StandardCharsets.UTF_8);
                    System.out.println("客户端说:" + line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

添加循环操作后,发现依然无法实现

原因在于:
外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作
在这里插入图片描述

使用多线程实现多客户端连接服务端

若想使一个服务端可以支持多客户端连接,我们需要解决以下问题

  • 循环调用accept方法侦听客户端的连接
  • 使用线程来处理单一客户端的数据交互

因为需要处理多客户端,所以服务端要循环调用accept方法,但该方法会产生阻塞,且读取客户端的消息也是依靠一个循环完成的,这会导致它接近是一个死循环,不结束的话也会影响服务端再次调用accept方法,所以与某个客户端的交互就需要使用线程来并发处理

服务端代码改造:

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * 聊天室服务端
 */
public class Server {
    private ServerSocket serverSocket;

    public Server() {
        try {
            /*
             * 运行在服务端的ServerSocket可以理解为是电话的"总机",可以同时接受多个
             * 客户端的链接。
             * ServerSocket创建时要向系统申请服务端口,客户端就是通过这个端口与服务端
             * 建立链接的,并且ServerSocket要监听服务端口,这样一旦一个客户端链接ServerSocket
             * 会立即创建一个Socket与开客户端交互
             * 需要注意,实例化ServerSocket指定的服务端口不能与当前系统其他应用程序
             * 开启的服务端口冲突,否则会抛出异常:
             * java.net.BindException:address already in use
             */
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
             * ServerSocket提供的重要方法:
             * Socket accept()
             * 该方法用于等待并接收客户端的链接,一旦一个客户端链接accept方法会立即
             * 返回一个Socket实例,通过这个Socket就可以与刚建立链接的客户端交互。
             * 如果没有客户端链接,那么accept方法会处于阻塞状态进行等待.
             */
            while (true) {
                System.out.println("等待客户端链接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端链接了!");

                // 启动一条线程来处理与该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 线程任务,负责与指定客户端进行交互
     */
    private class ClientHandler implements Runnable {
        private Socket socket;
        private String host; // 记录该客户端的IP地址信息

        public ClientHandler(Socket socket) {
            this.socket = socket;
            // 通过socket获取远端计算机IP地址
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {
            try {
                InputStream in = socket.getInputStream();
                int len;
                /*
                 * 首先单独读取1个字节,接收客户端发送过来的数据的长度
                 * 如果读取一个字节返回值为-1,则说明流读取到了末尾,对于这里来讲就是客户端
                 * 与我们断开连接,我们才无法再从流中读取字节
                 */
                while ((len = in.read()) != -1) {
                    byte[] data = new byte[len];
                    in.read(data);
                    String line = new String(data, StandardCharsets.UTF_8);
                    System.out.println("客户端说:" + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值