Java网络编程

Socket编程:理解TCP/IP套接字,如何在Java中实现客户端和服务端的通信。 URL与URLConnection:如何使用Java的URL和URLConnection类进行基本的HTTP请求。 HttpURLConnection与HttpClient:深入学习如何利用这些类库进行更复杂的HTTP请求和响应处理。

网络通信基础概念

IP地址和端口

  • IP地址(Internet Protocol Address):是网络中设备的唯一标识。IP地址是一种数字标识符,它遵循Internet Protocol(IP)规定的格式。有两种主要的IP地址版本:

    • IPv4:每个IP地址长32bit,也就是4个字节。如:11000000 10101000 00000001 01000010,十进制为:192.168.1.66
    • IPv6:128位地址长度,通常表示为 八组==四位十六进制数(每个十六进制4位,所以每16位一组)==。例如2001:0db8:85a3:0000:0000:8a2e:0370:7334
    java
    // java 关于IP地址的使用:
    // InetAddress inetAddress = InetAddress.getByName("LAPTOP-TS9EH1VR");
    InetAddress inetAddress = InetAddress.getByName("192.168.0.9");
    
    System.out.println(inetAddress.getHostName());     // LAPTOP-TS9EH1VR
    System.out.println(inetAddress.getHostAddress());  // 192.168.0.9

IP地址使得数据包能够在互联网上被路由和送达目标主机。


  • 端口(port): 端口是操作系统中的一种逻辑结构,用于区分不同的网络服务或应用程序。每个端口由一个16位的整数表示,范围从0到65535。端口的作用是:
    • 允许同一台主机上的多个应用程序同时使用网络。
    • 指定特定类型的数据应被哪个应用程序接收或发送。

常见的端口包括80(HTTP)、443(HTTPS)、22(SSH)、21(FTP)等。端口分为三类:熟知端口(0-1023,用于知名的网络服务和应用),注册端口(1024-49151,普通应用程序使用),动态或私有端口(49152-65535)。

协议(Protocol)

网络协议是一组规则,规定了网络上数据的格式、交换过程和动作序列。它定义了如何建立、维护和终止通信。一些常见的网络协议包括:

  • TCP (Transmission Control Protocol):提供可靠的、面向连接的数据传输服务。
  • UDP (User Datagram Protocol):提供简单的、无连接的数据传输服务,不保证数据的顺序或可靠性。
  • HTTP (Hypertext Transfer Protocol):用于Web通信的标准协议。
  • HTTPS:HTTP的安全版本,使用SSL/TLS加密数据。
  • FTP (File Transfer Protocol):用于文件上传和下载。
  • SMTP (Simple Mail Transfer Protocol):用于电子邮件传输。

Socket网络编程

在计算机网络中,Socket是网络上两个程序之间进行双向通信的端点。

具体来说Socket是一种抽象的==网络通信接口==,它允许一个程序与其他程序通信,无论是在同一台机器上还是通过网络。Socket可以基于不同的协议,如TCP或UDP。

在Java中,Socket类和ServerSocket类用于实现客户端和服务器之间的通信。具体来说:

  • Socket类:代表客户端的连接,用于向服务器发起连接请求。
  • ServerSocket类:代表服务器端的监听,用于接受客户端的连接请求。

Socket提供了读写数据的方法,如InputStreamOutputStream,用于发送和接收数据。此外,DatagramSocketDatagramPacket类用于基于UDP的通信,它们处理数据报包的发送和接收。

UDP协议及通信

在Java中,基于UDP协议的Socket编程主要涉及DatagramSocketDatagramPacket这两个类。UDP(用户数据报协议)是一种无连接的协议,它不保证数据的顺序和完整性,但是具有低延迟和高效率的特点,适用于不需要可靠传输的场合,如实时音频和视频流。

  • 无连接:在发送数据前无需建立连接。
  • 不可靠:没有确认机制,数据可能丢失、重复或乱序。
  • 广播和多播:可以利用UDP进行广播和多播通信。

下面是一个简单的Java UDP客户端和服务器示例:

UDP服务器端: 服务器监听端口1234,接收来自客户端的消息,并将收到的消息原样发回

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

public class UDPServer {
    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket(1234)) {
            byte[] buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            
            System.out.println("Server is ready to receive messages.");
            
            while (true) {
                socket.receive(packet);
                String received = new String(packet.getData(), 0, packet.getLength());
                System.out.println("Received from client: " + received);
                
                // Echo back to the client
                InetAddress address = packet.getAddress();
                int port = packet.getPort();
                packet = new DatagramPacket(buffer, buffer.length, address, port);
                socket.send(packet);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UDP客户端: 客户端则向服务器发送一条消息,并接收服务器的回应。

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

public class UDPClient {
    public static void main(String[] args) {
        try (DatagramSocket socket = new DatagramSocket()) {
            byte[] buffer = "Hello, Server!".getBytes();
            InetAddress address = InetAddress.getByName("localhost");
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, 1234);
            
            socket.send(packet);
            System.out.println("Message sent to server.");
            
            // Receive response
            buffer = new byte[1024];
            packet = new DatagramPacket(buffer, buffer.length);
            socket.receive(packet);
            String response = new String(packet.getData(), 0, packet.getLength());
            System.out.println("Response from server: " + response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCP/IP协议及通信

在Java中,基于TCP/IP协议的Socket编程主要是使用java.net.Socketjava.net.ServerSocket这两个核心类。TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,在网络通信中广泛用于需要高可靠性的数据传输。TCP Socket编程特点如下:

  • 面向连接:在数据传输之前必须先建立连接,传输完成后要释放连接。
  • 可靠传输:提供错误检测和自动重传,保证数据的顺序性和完整性。
  • 全双工:通信双方都可以同时发送和接收数据。

完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性, TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

下面是一个简单的Java TCP服务器和客户端的示例代码:

TCP服务器端:服务器监听端口1234,并为每个连接的客户端创建一个新的线程(在实际代码中通常使用线程池)。当任一端发送“bye”时,连接将被关闭。

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

public class TCPServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(1234)) {
            System.out.println("Server started. Listening on port 1234...");

            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("New client connected: " + clientSocket);

                // 创建一个新的线程来处理客户端的连接
                Thread clientHandler = new Thread(() -> {
                    try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                         PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
                        String inputLine;
                        while ((inputLine = in.readLine()) != null) {
                            System.out.println("Received from client: " + inputLine);
                            if ("bye".equalsIgnoreCase(inputLine)) {
                                break;
                            }
                            out.println("Echo: " + inputLine);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            clientSocket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                // 启动线程
                clientHandler.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCP客户端:客户端连接到服务器并可以发送任意数量的消息,服务器将这些消息回显给客户端。当任一端发送“bye”时,连接将被关闭。

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

public class TCPClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 1234);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
            System.out.println("Connected to server.");
            
            String fromServer;
            String fromUser;
            while ((fromUser = stdIn.readLine()) != null) {
                out.println(fromUser);
                out.flush();
                if ("bye".equalsIgnoreCase(fromUser)) {
                    break;
                }
                fromServer = in.readLine();
                System.out.println("Received from server: " + fromServer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

当前的服务器端代码中,每当一个客户端连接时,服务器就会创建一个新的线程来处理这个连接。这样,即使有多个客户端同时连接,服务器也能有效地处理每个客户端的请求。然而,这种实现可能会消耗大量的系统资源,特别是在大量客户端连接的情况下,因为每个连接都会启动一个新的线程。在生产环境中,更推荐使用线程池来限制并发线程的数量,从而更有效地管理资源。

NIO和AIO

NIO(Non-blocking I/O)和AIO(Asynchronous I/O)是Java中用于提高I/O操作性能的两种高级机制,它们可以显著提升在网络编程中的并发能力和响应速度。

  • 非阻塞I/O(NIO)或多路复用(如select/poll):使用Java NIO的Selector,可以在单一线程中管理多个通道(Channels)的读写操作,而无需为每个连接创建单独的线程。

  • 异步I/O(AIO):在Java中使用AIO模型,可以注册通道的读写事件,当事件就绪时通过回调处理,这也是非阻塞且高效的处理方式。

NIO(Non-blocking I/O)引入了通道(Channel)和缓冲区(Buffer)的概念,其中通道可以是文件、网络连接或其他数据源,而缓冲区则用于存储待处理的数据。NIO的主要优点是支持非阻塞I/O,即在没有数据可读或写时,不会阻塞线程,从而提高了服务器的并发能力。

使用NIO优化服务器代码:

  1. 使用ServerSocketChannel:替代传统的ServerSocket,创建一个非阻塞的ServerSocketChannel
  2. 使用SelectorSelector用于监控多个Channel的I/O状况,当某个Channel准备好进行读写操作时,Selector会通知相应的线程去处理。
  3. 使用ByteBuffer:用于读取和写入数据,代替InputStreamOutputStream
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioTcpServer {
    private final Selector selector;
    private final ServerSocketChannel serverChannel;
    private final ByteBuffer buffer = ByteBuffer.allocate(1024);

    public NioTcpServer(int port) throws IOException {
        selector = Selector.open();
        serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void listen() throws IOException {
        while (!Thread.currentThread().isInterrupted()) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                if (key.isAcceptable()) {
                    registerForRead((ServerSocketChannel) key.channel());
                } else if (key.isReadable()) {
                    readData(key);
                }
                it.remove();
            }
        }
    }

    private void registerForRead(ServerSocketChannel channel) throws IOException {
        SocketChannel clientChannel = channel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(selector, SelectionKey.OP_READ);
    }

    private void readData(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        buffer.clear();
        int numRead = channel.read(buffer);
        if (numRead > 0) {
            buffer.flip();
            byte[] data = new byte[numRead];
            buffer.get(data);
            System.out.println("Received: " + new String(data));
            writeData(channel, data);
        }
    }

    private void writeData(SocketChannel channel, byte[] data) throws IOException {
        buffer.clear();
        buffer.put(data);
        buffer.flip();
        channel.write(buffer);
    }

    public static void main(String[] args) throws IOException {
        new NioTcpServer(1234).listen();
    }
}

AIO(Asynchronous I/O)是NIO的扩展,提供了真正的异步I/O操作。在AIO中,你可以发起一个I/O操作并立即返回,当操作完成时,系统会通知你的程序。这对于高并发的服务器特别有用。

使用AIO优化服务器代码:

  1. 使用AsynchronousServerSocketChannel:创建一个监听特定端口的异步服务器通道。
  2. 使用Future:发起异步操作时,返回一个Future对象,可以用来检查操作是否完成或获取结果。

由于AIO在Java中是通过JDK 7引入的,其API可能不如NIO成熟和广泛使用,但在某些场景下,特别是高并发场景,AIO可以提供更好的性能。

注意,NIO和AIO的实现都比较复杂,需要对Java的I/O模型有深入的理解。在实际应用中,可能还需要结合线程池和其他并发工具来进一步优化性能。

网络编程注意事项

除了基本的错误与异常处理、多线程及线程池的使用外,还应该关注以下事项: