step

socket 网络编程

前置知识是学过计算机网络课程。

socket 简介

socket 也就是套接字,实现对 TCP UDP 协议的封装。

IP 地址和端口的结合协议。当数据报到达电脑后还需要直到发送到哪个进程中,而端口可以标识进程,所以 socket 实现了端口和 ip 地址的绑定。

同时也是 TCP/IP 协议相关 API 的总称;网络 API 的集合实现。

socket 的作用是实现了两个端点之间的链接。等同于 Socket = IP Address + Port Number

coding

环境 Win10 + Idea + Gradle

创建工程

  • 选择 gradle 创建一个项目。
  • 输入自定义的项目名称。
  • 等待加载,出现 BUILD SUCCESSFUL 说明自动加载成功。
  • 然后在 src/mian/java 文件夹下创建名称为 Server 和 Client 的 java 文件。前者代表服务端,后者代表客户端,结果如下。

客户端 Client

首先编写 Client 代码,基础代码如下。(为了便于叙述,后续贴入的都是关键代码,如遇报错,请使用快捷键 Alt + Enter 消除,最后会提供完整代码)

public class Client {
    public static void main(String[] args) throws Exception {
    }
}

然后创建一个 socket 对象。设置超时超时时间为 3000ms 也就是 3s。

setToTimeout 准确的来说指的是读取数据时阻塞链路的超时时间,这个时间一旦超过设定的时长就会抛出异常。

之后是连接了,连接服务器端 Serve ,设置超时时间,并设定相应的 IP 地址及端口。因为是同一台机器, IP 地址就是本机了,端口 设置为 2000 。

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

        socket.setSoTimeout(3000);
        // 设置连接的超时时间为 3000ms;设置IP地址和端口号,IP地址是本机也就是 LocalHost ,端口为 2000
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(),2000),3000);

        System.out.println("Server connection initiated!");
        System.out.println("Client information:"+socket.getLocalAddress()+ "P:" + socket.getLocalPort());
        System.out.println("Server information:"+socket.getInetAddress()+ "P:" + socket.getPort());
    }
}

此时链接已经建成,然后构造一个函数 todo ,其功能是将客户端的数据发送到服务器并从服务器中接收数据。

抽象一下,假设 todo 已经写好,将其置于异常之中。这是自顶向下的编程方法,之后专注于实现 todo 函数。

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

        // 设置读取流超时的时间,单位是 ms
        socket.setSoTimeout(3000);
        // 设置连接的超时时间为 3000ms;设置IP地址和端口号,IP地址是本机也就是 LocalHost ,端口为 2000
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(),2000),3000);

        System.out.println("Server connection initiated!");
        System.out.println("Client information:"+socket.getLocalAddress()+ "P:" + socket.getLocalPort());
        System.out.println("Server information:"+socket.getInetAddress()+ "P:" + socket.getPort());
        try {
            todo(socket);
        } catch (IOException e) {
            System.out.println("Abnormal shoutdown");
        }
        socket.close();
        System.out.println("Client exicted");
    }
}

todo 函数的目的是将数据发送到服务器中,首先需要将键盘上敲入的字符存入变量之中。

构建基本的输入流,用于读取键盘输入的字符。

    private static void todo(Socket client) throws IOException {
        // 构建输入流用于从键盘输入数据
        InputStream in = System.in;
        BufferedReader input = new BufferedReader(new InputStreamReader(in));

    }

分别设置和服务器端交互的对象。

    private static void todo(Socket client) throws IOException {
        // 构建输入流用于从键盘输入数据
        InputStream in = System.in;
        BufferedReader input = new BufferedReader(new InputStreamReader(in));

        // 构建 socket 输出流,拿到 Socket 的输出流并转换为打印流
        OutputStream outputStream = client.getOutputStream();
        PrintStream socketPrintStream = new PrintStream(outputStream);

        // 构建 socket 输入流,并转换为 BufferedReader
        InputStream inputStream = client.getInputStream();
        BufferedReader socketBufferredReader = new BufferedReader(new InputStreamReader(inputStream));

    }

设循环,使用之上写好的对象。

        boolean flag = true;
        do {
            // 键盘读取数据并发送到服务器
            String str = input.readLine();
            socketPrintStream.println(str);

            // 接收服务器发送过来的数据。
            String echo = socketBufferredReader.readLine();

            if ("bye".equalsIgnoreCase(echo)) {
                flag = false;
            }else {
                System.out.println(echo);
            }
        }while (flag);

完整如下:

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

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

        // 设置读取流超时的时间,单位是 ms
        socket.setSoTimeout(3000);
        // 设置连接的超时时间为 3000ms;设置IP地址和端口号,IP地址是本机也就是 LocalHost ,端口为 2000
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(),2000),3000);

        System.out.println("Server connection initiated!");
        System.out.println("Client information:"+socket.getLocalAddress()+ "P:" + socket.getLocalPort());
        System.out.println("Server information:"+socket.getInetAddress()+ "P:" + socket.getPort());
        try {
            todo(socket);
        } catch (IOException e) {
            System.out.println("Abnormal shoutdown");
        }
        socket.close();
        System.out.println("Client exicted");
    }
    private static void todo(Socket client) throws IOException {
        // 构建输入流用于从键盘输入数据
        InputStream in = System.in;
        BufferedReader input = new BufferedReader(new InputStreamReader(in));

        // 构建 socket 输出流,拿到 Socket 的输出流并转换为打印流
        OutputStream outputStream = client.getOutputStream();
        PrintStream socketPrintStream = new PrintStream(outputStream);

        // 构建 socket 输入流,并转换为 BufferedReader
        InputStream inputStream = client.getInputStream();
        BufferedReader socketBufferredReader = new BufferedReader(new InputStreamReader(inputStream));

        boolean flag = true;
        do {
            // 键盘读取数据并发送到服务器
            String str = input.readLine();
            socketPrintStream.println(str);

            // 接收服务器发送过来的数据。
            String echo = socketBufferredReader.readLine();

            if ("bye".equalsIgnoreCase(echo)) {
                flag = false;
            }else {
                System.out.println(echo);
            }
        }while (flag);

        // 资源释放
        socketPrintStream.close();
        socketBufferredReader.close();
    }
}

服务器端 Server

完整代码如下,运行即可。注意 idea 存在乱码问题。

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

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(2000);

        System.out.println("Service ready!");
        System.out.println("Service information:" + server.getInetAddress() + "P:" + server.getLocalPort());

        // 等待客户端连接
        for(;;){
            // 得到客户端
            Socket client = server.accept();
            // 构建异步线程
            ClientHandler clientHandler = new ClientHandler(client);
            // 启动异步线程
            clientHandler.start();
        }
    }

    // 客户端消息处理
    private static class ClientHandler extends Thread {
        private Socket socket;
        private boolean flag = true;

        ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            super.run();
            System.out.println("New client connection:" + socket.getInetAddress() + "P:" + socket.getPort());

            try {
                // 打印流,用于输出数据,向 客户端发送数据。
                PrintStream socketOutput = new PrintStream(socket.getOutputStream());

                // 接收字符,用于接收数据,接收客户端发送过来的数据。
                BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                do {

                    // 存放客户端发送过来的数据
                    String str = socketInput.readLine();
                    if ("bye".equalsIgnoreCase(str)) {
                        flag = true;
                        socketOutput.println("bye");
                     } else {
                        // 将收到的数据打印到屏幕上
                        System.out.println(str);
                        socketOutput.println("Echo message:" + str.length());
                    }
                }while (flag);

                socketInput.close();
                socketOutput.close();

            }catch (Exception e){
                System.out.println("Abnormally disconnectioned!");
            }finally {
                try {
                    socket.close();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("The client has exited:" + socket.getInetAddress() + "P:" + socket.getPort());
        }

    }
}

首先启动 Server 文件,启动成功后,得到如下结果。

然后启动 Client 文件,结果如下。

接下来就是在两个终端中交互信息了。客户端发送消息,服务端接收,触发 bye 关键词后断开链接。

服务端的信息如下。

UDP

创建 UDPProvider 和 UDPSearcher 两个文件,前者用于接收发过来的 UDP 数据然后在返回给发送者,实现信息交互功能。

首先指定一个端口用于监测接收到的 UDP 数据包。

UDPProvider

public class UDPProvider {
    public static void main(String[] args) throws SocketException {
        System.out.println("UDPProver Started.");

        // 指定端口用于数据接收
        DatagramSocket ds = new DatagramSocket(20000);
    }
}

创建一个数组用于存放接收数据到的 UDP 数据。

public class UDPProvider {
    public static void main(String[] args) throws IOException {
        // ...

        // 创建一个实体,用于接收数据
        final byte[] buf = new byte[512];
        DatagramPacket receivePack = new DatagramPacket(buf,buf.length);

        // 接收数据
        ds.receive(receivePack);
    }
}

接下来就是将发送过来的数据中的信息提取出来了。

public class UDPProvider {
    public static void main(String[] args) throws IOException {
        // ...
        
        // 提取接收数据中的信息
        String ip= receivePack.getAddress().getHostAddress();
        int port= receivePack.getPort();
        int dateLen = receivePack.getLength();
        String data = new String(receivePack.getData(),0,dateLen);
        System.out.println("UDPProvider receive from ip :" + ip + "\tport:" + port + "\tdata:" + data);

        
    }
}

之后就是拿到数据后在回送一份数据给发送者。完整代码如下:

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

public class UDPProvider {
    public static void main(String[] args) throws IOException {
        System.out.println("UDPProver Started.");

        // 创建对象并指定其端口用于接收数据
        DatagramSocket ds = new DatagramSocket(20000);

        // 创建一个实体,用于接收数据
        final byte[] buf = new byte[512];
        DatagramPacket receivePack = new DatagramPacket(buf,buf.length);

        // 接收数据
        ds.receive(receivePack);

        // 提取接收数据中的信息
        String ip= receivePack.getAddress().getHostAddress();
        int port= receivePack.getPort();
        int dateLen = receivePack.getLength();
        String data = new String(receivePack.getData(),0,dateLen);
        System.out.println("UDPProvider receive from ip :" + ip + "\tport:" + port + "\tdata:" + data);

        // 回送一份数据,将发送过来的数据再发送回去
        String responseData = "Receive data with len:" + dateLen;
        byte[] responseDataBytes = responseData.getBytes();
        DatagramPacket responsePacket = new DatagramPacket(responseDataBytes,
            responseDataBytes.length,
            receivePack.getAddress(),
            receivePack.getPort());
        ds.send(responsePacket);

        System.out.println("UDPProver Finished.");
        ds.close();
    }
}

UDPSearcher

作为发送数据的一方,首先要设置自己的端口,和接收方不同的是,这里不用指定端口,系统随机分配即可。

接收方是必须指定端口的,不然发送者是不知道将数据发送到何地。

和接收方类似,调整一下顺序即可,完整代码如下:

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

public class UDPSearcher {
    public static void main(String[] args) throws IOException {
        System.out.println("UDPSearcher Started.");

        // 系统分配端口
        DatagramSocket ds = new DatagramSocket();

        // 发送一个 Hello word 信息,存入字节数组中
        String requestsData = "Hello word";
        byte[] requestsDataBytes = requestsData.getBytes();

        DatagramPacket requestsPacket = new DatagramPacket(requestsDataBytes,requestsDataBytes.length);

        // 设置目的地的 IP 和端口,由于目的地是本机,即 LocalHost ,而端口为 20000
        requestsPacket.setAddress(InetAddress.getLocalHost());
        requestsPacket.setPort(20000);
        ds.send(requestsPacket);

        // 创建一个实体,用于接收数据
        final byte[] buf = new byte[512];
        DatagramPacket receivePack = new DatagramPacket(buf,buf.length);

        // 接收数据
        ds.receive(receivePack);

        // 提取接收数据中的信息
        String ip= receivePack.getAddress().getHostAddress();
        int port= receivePack.getPort();
        int dateLen = receivePack.getLength();
        String data = new String(receivePack.getData(),0,dateLen);
        System.out.println("UDPSearcher receive from ip :" + ip + "\tport:" + port + "\tdata:" + data);

        System.out.println("UDPSearcher Finished.");
        ds.close();
    }
}

首先启动接收方,结果如下:

然后启动发送方,结果如下:

运行成功,采用 UDP 协议将数据发送过去被提取出来,接收到数据后在提取出来。

Tagged with 计算机网络

Posted June 30, 2020


WIJE picweijiew . github