Java网络编程:从入门到精通

1. 网络编程入门

1.1 网络编程概述

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

网络编程则是在网络通信协议下,不同计算机上运行的程序进行数据传输的过程。

1.2 网络编程三要素

  1. IP地址:网络中设备的唯一标识,用于指定接收数据的计算机和识别发送数据的计算机。

  2. 端口:设备上应用程序的唯一标识。如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序。

  3. 协议:计算机网络中,连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定。常见的协议有UDP协议和TCP协议。

1.3 IP地址

IP地址是网络中设备的唯一标识,主要分为两大类:

  • IPv4:给每个连接在网络上的主机分配一个32位地址,通常以点分十进制表示(如192.168.1.66)

  • IPv6:采用128位地址长度,每16个字节一组,分成8组十六进制数,解决了IPv4地址空间不足的问题

常用DOS命令

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

特殊IP地址

  • 127.0.0.1:回送地址,可以代表本机地址,一般用来测试使用

1.4 InetAddress类

InetAddress类表示Internet协议(IP)地址,常用方法:

方法名 说明
static InetAddress getByName(String host) 确定主机名称的IP地址
String getHostName() 获取此IP地址的主机名
String getHostAddress() 返回IP地址字符串

代码演示

java 复制代码
import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
        InetAddress address = InetAddress.getByName("192.168.1.66");

        // 获取主机名
        String name = address.getHostName();
        // 获取IP地址
        String ip = address.getHostAddress();

        System.out.println("主机名:" + name);
        System.out.println("IP地址:" + ip);
    }
}

1.5 端口和协议

端口是设备上应用程序的唯一标识,端口号用两个字节表示,取值范围是0-65535。其中0-1023之间的端口号用于知名网络服务,普通应用程序应使用1024以上的端口号。

常见协议

  1. UDP协议(用户数据报协议):

    • 无连接通信协议,发送端不确认接收端是否存在就发送数据
    • 消耗系统资源小,通信效率高
    • 不能保证数据的完整性,适用于音频、视频传输等对实时性要求高但对完整性要求不高的场景
  2. TCP协议(传输控制协议):

    • 面向连接的通信协议,传输数据前需建立逻辑连接
    • 提供可靠无差错的数据传输
    • 连接建立需要"三次握手",断开连接需要"四次挥手"
    • 适用于上传文件、下载文件、浏览网页等需要可靠传输的场景

2. UDP通信程序

2.1 UDP发送数据

Java中使用DatagramSocket类作为基于UDP协议的Socket,发送数据步骤:

  1. 创建发送端的Socket对象(DatagramSocket)
  2. 创建数据,并把数据打包
  3. 调用DatagramSocket对象的方法发送数据
  4. 关闭发送端

代码演示

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端的Socket对象
        DatagramSocket ds = new DatagramSocket();

        // 创建数据,并把数据打包
        byte[] bys = "hello,udp,我来了".getBytes();
        DatagramPacket dp = new DatagramPacket(bys, bys.length, 
                                             InetAddress.getByName("127.0.0.1"), 10086);

        // 发送数据
        ds.send(dp);

        // 关闭发送端
        ds.close();
    }
}

2.2 UDP接收数据

接收数据步骤:

  1. 创建接收端的Socket对象(DatagramSocket)
  2. 创建一个数据包,用于接收数据
  3. 调用DatagramSocket对象的方法接收数据
  4. 解析数据包,并把数据在控制台显示
  5. 关闭接收端

代码演示

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // 创建接收端的Socket对象
        DatagramSocket ds = new DatagramSocket(12345);

        // 创建一个数据包,用于接收数据
        byte[] bys = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bys, bys.length);

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

        // 解析数据包,并显示数据
        System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
        
        // 关闭接收端
        ds.close();
    }
}

2.3 UDP通信程序练习

案例需求

  • UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
  • UDP接收数据:采用死循环接收,因为接收端不知道发送端什么时候停止发送

发送端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端的Socket对象
        DatagramSocket ds = new DatagramSocket();
        // 键盘录入数据
        Scanner sc = new Scanner(System.in);
        
        while (true) {
            String s = sc.nextLine();
            // 输入886,结束发送
            if ("886".equals(s)) {
                break;
            }
            // 创建数据,并把数据打包
            byte[] bys = s.getBytes();
            DatagramPacket dp = new DatagramPacket(bys, bys.length, 
                                                 InetAddress.getByName("192.168.1.66"), 12345);
            // 发送数据
            ds.send(dp);
        }
        // 关闭发送端
        ds.close();
    }
}

接收端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // 创建接收端的Socket对象
        DatagramSocket ds = new DatagramSocket(12345);
        
        while (true) {
            // 创建一个数据包,用于接收数据
            byte[] bys = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bys, bys.length);
            // 接收数据
            ds.receive(dp);
            // 解析数据包,并显示数据
            System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
        }
    }
}

2.4 UDP三种通讯方式

  1. 单播:用于两个主机之间的端对端通信
  2. 组播:用于对一组特定的主机进行通信
  3. 广播:用于一个主机对整个局域网上所有主机进行数据通信

2.5 UDP组播实现

发送端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class ClinetDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端的Socket对象
        DatagramSocket ds = new DatagramSocket();
        String s = "hello 组播";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("224.0.1.0");
        int port = 10000;
        
        // 创建数据,并把数据打包
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
        // 发送数据
        ds.send(dp);
        // 释放资源
        ds.close();
    }
}

接收端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 创建接收端Socket对象
        MulticastSocket ms = new MulticastSocket(10000);
        // 创建一个箱子,用于接收数据
        DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
        // 把当前计算机绑定一个组播地址,表示添加到这一组中
        ms.joinGroup(InetAddress.getByName("224.0.1.0"));
        // 接收数据
        ms.receive(dp);
        // 解析数据包,并打印数据
        byte[] data = dp.getData();
        int length = dp.getLength();
        System.out.println(new String(data, 0, length));
        // 释放资源
        ms.close();
    }
}

2.6 UDP广播实现

发送端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端Socket对象
        DatagramSocket ds = new DatagramSocket();
        // 创建存储数据的箱子,将广播地址封装进去
        String s = "广播 hello";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("255.255.255.255");
        int port = 10000;
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
        // 发送数据
        ds.send(dp);
        // 释放资源
        ds.close();
    }
}

接收端代码

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 创建接收端的Socket对象
        DatagramSocket ds = new DatagramSocket(10000);
        // 创建一个数据包,用于接收数据
        DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
        // 接收数据
        ds.receive(dp);
        // 解析数据包,并显示数据
        byte[] data = dp.getData();
        int length = dp.getLength();
        System.out.println(new String(data, 0, length));
        // 关闭接收端
        ds.close();
    }
}

3. TCP通信程序

3.1 TCP发送数据

Java为客户端提供了Socket类,为服务器端提供了ServerSocket类。

发送端代码

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        // 创建Socket对象,连接服务端
        Socket socket = new Socket("127.0.0.1", 10000);

        // 从连接通道中获取输出流
        OutputStream os = socket.getOutputStream();
        // 写出数据
        os.write("aaa".getBytes());

        // 释放资源
        os.close();
        socket.close();
    }
}

3.2 TCP接收数据

接收端代码

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket ss = new ServerSocket(10000);

        // 监听客户端的链接
        Socket socket = ss.accept();

        // 从连接通道中获取输入流读取数据
        InputStream is = socket.getInputStream();
        int b;
        while ((b = is.read()) != -1) {
            System.out.println((char) b);
        }

        // 释放资源
        socket.close();
        ss.close();
    }
}

注意:

  • accept()方法是阻塞的,等待客户端连接
  • 客户端创建对象并连接服务器时,通过三次握手协议保证连接
  • read()方法也是阻塞的
  • 客户端关闭流时,会往服务器写结束标记
  • 断开连接通过四次挥手协议保证

3.3 TCP程序练习(传输中文)

发送端代码

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        // 创建Socket对象并连接服务端
        Socket socket = new Socket("127.0.0.1", 10000);

        // 从连接通道中获取输出流
        OutputStream os = socket.getOutputStream();
        // 写出数据
        os.write("你好你好".getBytes());

        // 释放资源
        os.close();
        socket.close();
    }
}

接收端代码

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket ss = new ServerSocket(10000);

        // 监听客户端的链接
        Socket socket = ss.accept();

        // 从连接通道中获取输入流读取数据
        InputStream is = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        
        int b;
        while ((b = isr.read()) != -1) {
            System.out.print((char) b);
        }

        // 释放资源
        socket.close();
        ss.close();
    }
}

4. 综合练习

练习一:多发多收

需求

  • 客户端:多次发送数据
  • 服务器:多次接收数据,并打印

客户端代码

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        // 创建Socket对象并连接服务端
        Socket socket = new Socket("127.0.0.1", 10000);

        // 写出数据
        Scanner sc = new Scanner(System.in);
        OutputStream os = socket.getOutputStream();

        while (true) {
            System.out.println("请输入您要发送的信息");
            String str = sc.nextLine();
            if ("886".equals(str)) {
                break;
            }
            os.write(str.getBytes());
        }
        
        // 释放资源
        socket.close();
    }
}

服务器代码

java 复制代码
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象绑定10000端口
        ServerSocket ss = new ServerSocket(10000);

        // 等待客户端来连接
        Socket socket = ss.accept();

        // 读取数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        int b;
        while ((b = isr.read()) != -1) {
            System.out.print((char) b);
        }

        // 释放资源
        socket.close();
        ss.close();
    }
}

练习二:接收并反馈

案例需求

  • 客户端:发送数据,接受服务器反馈
  • 服务器:收到消息后给出反馈

客户端代码

java 复制代码
import java.io.*;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10000);

        OutputStream os = socket.getOutputStream();
        os.write("hello".getBytes());
        
        // 仅仅关闭输出流,并写一个结束标记,对socket没有任何影响
        socket.shutdownOutput();
        
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        
        br.close();
        os.close();
        socket.close();
    }
}

服务器代码

java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        Socket accept = ss.accept();

        InputStream is = accept.getInputStream();
        int b;
        while ((b = is.read()) != -1) {
            System.out.println((char) b);
        }

        System.out.println("看看我执行了吗?");

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
        bw.write("你谁啊?");
        bw.newLine();
        bw.flush();

        bw.close();
        is.close();
        accept.close();
        ss.close();
    }
}

练习三:文件上传(TCP协议)

案例需求

  • 客户端:数据来自于本地文件,接收服务器反馈
  • 服务器:接收到的数据写入本地文件,给出反馈

客户端代码

java 复制代码
import java.io.*;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        // 创建Socket对象,并连接服务器
        Socket socket = new Socket("127.0.0.1", 10000);

        // 读取本地文件中的数据,并写到服务器当中
        BufferedInputStream bis = new BufferedInputStream(
            new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        // 往服务器写出结束标记
        socket.shutdownOutput();

        // 接收服务器的回写数据
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();
        System.out.println(line);

        // 释放资源
        socket.close();
    }
}

服务器代码

java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        // 等待客户端来连接
        Socket socket = ss.accept();

        // 读取数据并保存到本地文件中
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        BufferedOutputStream bos = new BufferedOutputStream(
            new FileOutputStream("mysocketnet\\serverdir\\a.jpg"));
        
        int len;
        byte[] bytes = new byte[1024];
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
        bos.close();
        
        // 回写数据
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("上传成功");
        bw.newLine();
        bw.flush();

        // 释放资源
        socket.close();
        ss.close();
    }
}

练习四:解决文件名重复问题

使用UUID生成唯一文件名:

java 复制代码
import java.util.UUID;

public class UUIDTest { 
    public static void main(String[] args) { 
        String str = UUID.randomUUID().toString().replace("-", ""); 
        System.out.println(str); // 例如:9f15b8c356c54f55bfcb0ee3023fce8a 
    } 
}

修改服务器代码,使用UUID作为文件名:

java 复制代码
// 服务器代码片段
String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));

练习五:服务器改写为多线程

让服务器可以同时处理多个客户端请求:

java 复制代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            // 等待客户端来连接
            Socket socket = ss.accept();

            // 开启一条线程处理客户端请求
            new Thread(new MyRunnable(socket)).start();
        }
    }
}

class MyRunnable implements Runnable {
    Socket socket;

    public MyRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 读取数据并保存到本地文件中
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
            
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            bos.close();
            
            // 回写数据
            BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

练习六:线程池改进

使用线程池管理线程资源,提高服务器性能:

java 复制代码
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3, // 核心线程数量
            16, // 线程池总大小
            60, // 空闲时间
            TimeUnit.SECONDS, // 空闲时间单位
            new ArrayBlockingQueue<>(2), // 队列
            Executors.defaultThreadFactory(), // 线程工厂
            new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        // 创建ServerSocket对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            // 等待客户端来连接
            Socket socket = ss.accept();

            // 使用线程池处理客户端请求
            pool.submit(new MyRunnable(socket));
        }
    }
}

总结

本文详细介绍了Java网络编程的基础知识和实践应用,包括:

  1. 网络编程的基本概念和三要素(IP地址、端口、协议)
  2. UDP协议通信的实现方式,包括单播、组播和广播
  3. TCP协议通信的实现方式,包括三次握手和四次挥手机制
  4. 多个综合练习,从简单的收发数据到文件上传,再到使用多线程和线程池提高服务器性能