Java 网络编程

文章目录

  • UDP Socket API
    • DatagramSocket
    • DatagramPacket
    • 例子:UDP版本的回显服务器-客户端
  • TCP Socket API
    • ServerSocket
    • Socket
    • 例子:TCP版本的回显服务器-客户端

UDP Socket API

DatagramSocket

这是一个 socket 类,本质上相当于一个文件,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。

构造一个 DatagramSocket 对象,就相当于打开了一个内核中的 socket 文件

构造方法:

构造方法 说明
DatagramSocket() 构造一个数据报套接字,并将其绑定到本地主机上的任何可用端口
DatagramSocket(int port) 构造一个数据报套接字,并将其绑定到本地主机上的指定端口

普通方法:

方法 说明
void receive(DatagramPacket p) 从该套接字接收数据报,此方法会一直阻塞,直到接收到数据报为止
void send(DatagramPacket p) 从此套接字发送数据报
void close() 关闭此数据报套接字

DatagramPacket

表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据就是以 DatagramPacket 为基本单位

构造方法 说明
DatagramPacket(byte buf[], int length) 构造一个 DatagramPacket,用于接收长度为 length 的数据包,length 参数必须小于或等于 buf.length
DatagramPacket(byte buf[], int length, SocketAddress address) 构造一个数据报,用于将长度为 length 的数据报发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length
DatagramPacket(byte buf[], int offset, int length, SocketAddress address) 构造一个数据报,用于将偏移量为 ioffsetlength 长度的数据报发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length
DatagramPacket(byte buf[], int length, InetAddress address, int port) 构造一个数据报,用于将长度为 length 的数据包发送到指定主机上的指定端口号。length 参数必须小于或等于 buf.length
方法 说明
InetAddress getAddress() 返回发送此数据报或接收数据报的机器的 IP 地址
SocketAddress getSocketAddress() 获取此数据包发送到或来自的远程主机的 SocketAddress(通常为IP地址+端口号)
int getPort() 返回发送的数据报中的接收端主机端口号,或者接收的数据报中的发送端主机端口号
byte[] getData() 返回数据缓冲区
int getLength() 返回要发送的数据长度或接收的数据长度

InetSocketAddress

创建 DatagramPacket 时,需要 SocketAddress,该对象通过 InetSocketAddress 创建。

构造方法 说明
InetSocketAddress(InetAddress addr, int port) 根据 IP 地址和端口号创建套接字地址

例子:UDP版本的回显服务器-客户端

服务器:

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

public class UDPEchoServer {
    private final DatagramSocket socket;

    public UDPEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器 启动!");
        while (true) {
            // 读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); // 空的,用于接收响应
            socket.receive(requestPacket);
            // 对请求进行解析,把 DatagramPacket 转成 String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            // 处理响应
            String response = process(request);
            // 把响应构造成 DatagramPacket 对象
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requestPacket.getSocketAddress());
            // 把响应发送给客户端
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req=%s;resp=%s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }

    public String process(String request) {
        return request;
    }

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

客户端:

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

public class UDPEchoClient {
    private final DatagramSocket socket;

    public UDPEchoClient() throws SocketException {
        // 客户端端口一般自动分配
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 客户端从控制台读取数据
            System.out.print("> ");
            String request = scanner.next();
            // 构造 DatagramPacket
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName("127.0.0.1"), 8000);
            // 发送给服务器
            socket.send(requestPacket);
            // 从服务器读取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 把响应数据转成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            System.out.printf("req: %s; resp: %s\n", request, response);
        }
    }

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

因为这里的 socket 创建出来就会一直用,所以是伴随程序的整个生命周期的,所以不需要手动调用 close() 去关闭

技巧:Windows 使用 netstat -ano | findstr "端口号",可以查找占用该端口的进程pid

TCP Socket API

ServerSocket

ServerSocket 是创建 TCP 服务端 Socket 的 API

构造方法 说明
ServerSocket(int port) 创建绑定到指定端口的服务器套接字
方法 说明
Socket accept() 侦听要与此套接字建立的连接并接受该连接。该方法将阻塞,直到建立连接为止。
void close() 关闭此套接字,相当于发送 FIN

Socket

构造方法 说明
Socket(String host, int port) 创建流套接字并将其连接到命名主机上的指定端口号
方法 说明
InetAddress getInetAddress() 返回套接字所连接的地址
int getPort() 返回此套接字所连接的远程端口号
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

例子:TCP版本的回显服务器-客户端

服务端:

package network;

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 final 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();
            // 创建新线程去完成工作,主线程继续accept
            Thread t = new Thread(() -> {
                try {
                    processConnect(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

    // 短连接:一个连接只进行一次数据交互(一个请求 + 一个响应)
    // 长连接:一个连接进行多次数据交互(N 个请求 + N 个响应)
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            // 长连接
            while (true) {
                if (!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 读取请求并解析
                String request = scanner.next();
                // 根据请求计算响应
                String response = process(request);
                // 把响应写回客户端
                printWriter.println(response); // 注意补上空白符,如换行,对面next读的时候要读到空白符才往下走
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
            }
        } finally {
            // 一定要记得关闭 clientSocket
            // 因为它是 accept 创建出来的,每来一个连接就会创建一个,占用文件描述符资源
            clientSocket.close();
        }
    }

    public String process(String request) {
        return request;
    }

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

客户端:

package network;

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 final Socket socket;

    public TCPEchoClient() throws IOException {
        socket = new Socket("127.0.0.1", 8000); // 此时触发三次握手
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                // 从控制台读取用户输入
                System.out.print("> ");
                String request = scanner.next();
                // 把请求发送给服务器
                printWriter.println(request); // 注意补上空白符,如换行,对面next读的时候要读到空白符才往下走
                printWriter.flush();
                // 从服务器读取响应
                String response = scannerNet.next();
                System.out.printf("req: %s; resp: %s\n", request, response);
            }
        }
    }

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

上述服务端代码还可以使用线程池改进:

public void start() throws IOException {
    System.out.println("服务器 启动!");
    // 使用线程池,适合写自动扩容版本的
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        service.submit(() -> {
            try {
                processConnect(clientSocket);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

注意

这里的 TCP 服务器之所以使用多线程,是因为处理的是长连接,与客户端建立好连接之后,什么时候断开连接不确定,这一个连接里要处理多少请求,也不确定,单线程处理连接里的循环的时候,就无法 accept 新的连接了。

如果是短连接,每次连接只处理一个请求,就可以不使用多线程了。

你可能感兴趣的:(Java,网络,java,开发语言)