目录
- TCP/UDP协议概述
- Java网络编程基础类
- TCP网络传输
- UDP网络传输
- 常见异常处理
一、TCP/UDP协议概述
协议定义
- TCP(Transmission Control Protocol):传输控制协议/网间协议
- UDP(User Datagram Protocol):用户数据包协议
- 层级:均属于传输层协议
TCP协议特点
TCP协议是面向连接、可靠的、基于字节流的通信协议。
主要特征:
- 面向连接的传输
- 两个使用TCP的主机在传输数据前必须先建立TCP连接
- 使用三次握手建立连接
- 可靠性强
- 确保传输数据的正确性
- 不出现丢失或乱序
- 端到端通信
- TCP连接通常是一个客户端和一个服务器端
- 不支持广播和多播
- 字节流方式
- 以字节为单位传输字节序列
- 字节流的解释由应用层负责
UDP协议特点
UDP是一种无连接的传输协议,提供面向事务的简单不可靠传输服务。
主要特征:
- 无连接
- 传输数据前不建立连接
- 发送端只管发送,不关心接收状态
- 接收端只读取收到的信息
- 无连接状态维护
- 不需要维护连接状态
- 一台服务器可同时向多个客户端传输相同消息
- 开销小
- UDP信息包头部仅8个字节(TCP为20个字节)
- 额外开销很小
- 高吞吐量
- 不受拥塞控制算法调节
- 只受应用软件生成数据速率、传输带宽、主机性能限制
- 尽力而为交付
- 不保证可靠交付
- 主机不需要维持复杂的连接状态表
- 面向报文
- 保留报文边界
- 不拆分也不合并报文
- 应用程序需要选择合适的报文大小
二、Java网络编程基础类
InetAddress类
表示互联网协议IP地址的类。
主要方法:
| 方法 | 返回类型 | 描述 |
|---|---|---|
| equals(Object obj) | boolean | 将此对象与指定对象比较 |
| getAddress() | byte[] | 返回此InetAddress对象的原始IP地址 |
| getAllByName(String host) | static InetAddress[] | 返回指定主机名的所有IP地址数组 |
| getByAddress(byte[] addr) | static InetAddress | 根据原始IP返回InetAddress对象 |
| getByAddress(String host, byte[] addr) | static InetAddress | 根据主机名和IP创建InetAddress |
| getByName(String host) | static InetAddress | 根据主机名获取IP地址 |
| getHostAddress() | String | 返回IP地址字符串 |
| getHostName() | String | 返回主机名称(如未注册则返回IP地址) |
| getLocalHost() | static InetAddress | 返回本地主机 |
| toString() | String | 返回"主机名/IP地址"格式字符串 |
InetSocketAddress类
实现IP套接字地址,封装了IP和端口号。
三、TCP网络传输
核心概念
TCP传输建立时必须有客户端和服务端两个端点:
- 客户端:Socket
- 服务端:ServerSocket
由于TCP是面向连接的协议,Socket建立时会自动开启流对象进行数据操作。
Socket类(客户端)
实现客户端套接字,是两台机器间通信的端点。
构造方法:
Socket()
Socket(InetAddress address, int port) // 连接指定IP和端口的服务端
Socket(String host, int port) // 连接指定主机名和端口
常用方法:
| 方法 | 描述 |
|---|---|
| bind(SocketAddress bindpoint) | 绑定到本地地址 |
| close() | 关闭套接字 |
| connect(SocketAddress endpoint) | 将此套接字连接到服务器 |
| getInetAddress() | 获取套接字连接的地址 |
| getPort() | 获取套接字连接的端口 |
| getInputStream() | 获取输入流 |
| getOutputStream() | 获取输出流 |
ServerSocket类(服务端)
实现服务器套接字,通过accept方法获取连接的Socket对象。
构造方法:
ServerSocket()
ServerSocket(int port) // 监听指定端口
常用方法:
| 方法 | 描述 |
|---|---|
| accept() | 监听并接受连接(阻塞式方法) |
| bind(SocketAddress endpoint) | 绑定到特定地址 |
| close() | 关闭套接字 |
| getInetAddress() | 获取套接字地址 |
| getLocalPort() | 返回监听端口 |
TCP传输完整示例
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class TCPExample {
// 客户端和服务器需要分别封装成两个线程
public static void main(String[] args) throws IOException {
// 开启服务端线程
new Thread(new ServerSide()).start();
// 开启客户端线程
new Thread(new ClientSide()).start();
}
}
// 客户端线程
class ClientSide implements Runnable {
public void run() {
Socket s;
Scanner sc = new Scanner(System.in);
BufferedWriter bufw = null;
BufferedReader bufr;
try {
// 开启客户端连接本地11111端口
s = new Socket("127.0.0.1", 11111);
// 获取连接的输入输出流
bufw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bufr = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 客户端循环输入并获取服务端返回的字符串
while (sc.hasNext()) {
String scannerIn = sc.next();
// 将键盘录入写入Socket流中
bufw.write(scannerIn);
bufw.newLine(); // 服务端需要判断行结尾
bufw.flush();
// 如果输入q就退出循环关闭客户端
if (scannerIn.equals("q")) {
System.out.println("客户端关闭");
return;
}
// 打印从流中读取的服务端返回数据
System.out.println("返回的数据:" + bufr.readLine());
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 服务端线程
class ServerSide implements Runnable {
public void run() {
ServerSocket ss = null;
Socket s;
try {
// 初始化服务器套接字,监听11111端口
ss = new ServerSocket(11111);
// 获取客户端发来的套接字
s = ss.accept();
// 获取连接的输入输出流对象
BufferedReader bufr = new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while ((line = bufr.readLine()) != null) {
System.out.print("服务端接收数据:" + line + " ");
// 如果接收到q就关闭服务器
if (line.equals("q")) {
System.out.println("服务器关闭");
return;
}
// 反转字符串
StringBuilder sb = new StringBuilder(line);
sb.reverse();
String str1 = new String(sb);
// 将反转的字符串写入服务端的输出流中返回给客户端
bufw.write(str1);
bufw.newLine();
bufw.flush();
System.out.println("已返回");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、UDP网络传输
核心概念
UDP传输只需一个类:DatagramSocket
- 没有开启流对象
- 只是把数据封装成DatagramPacket对象进行发送或接收
DatagramSocket类
表示可接收可发送数据包的套接字(UDP套接字)。
构造方法:
DatagramSocket() // 绑定到任何可用端口
DatagramSocket(int port) // 绑定到指定端口
DatagramSocket(SocketAddress bindaddr) // 绑定到指定地址
DatagramSocket(int port, InetAddress laddr) // 绑定到指定端口和地址
常用方法:
| 方法 | 返回类型 | 描述 |
|---|---|---|
| bind(SocketAddress addr) | void | 将DatagramSocket绑定到特定地址和端口 |
| send(DatagramPacket p) | void | 从此套接字发送数据包 |
| receive(DatagramPacket p) | void | 从此套接字接收数据包(阻塞式方法) |
| close() | void | 关闭套接字 |
| getPort() | int | 返回此套接字的端口 |
| isBound() | boolean | 返回套接字的绑定状态 |
DatagramPacket类
表示数据报包(UDP在网络层中的传输单元)。
重要说明:
- 构造时必须设置数据缓冲区buf
- 发送时将buf封装到包中
- 接收时将包中的数据存入buf中
- 有地址参数的构造方法用于发送数据
构造方法:
// 用于接收数据
DatagramPacket(byte[] buf, int length)
// 用于发送数据
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
常用方法:
| 方法 | 返回类型 | 描述 |
|---|---|---|
| getAddress() | InetAddress | 返回数据报的目标/源IP地址 |
| getData() | byte[] | 返回数据缓冲区 |
| getLength() | int | 返回数据长度 |
| getPort() | int | 返回端口号 |
| getSocketAddress() | SocketAddress | 获取套接字地址 |
| setData(byte[] buf) | void | 设置数据缓冲区 |
| setAddress(InetAddress iaddr) | void | 设置地址 |
| setPort(int iport) | void | 设置端口 |
UDP传输示例
UDP发送端:
import java.net.*;
class UdpSend {
public static void main(String[] args) throws Exception {
// 1. 建立DatagramSocket服务
DatagramSocket ds = new DatagramSocket();
// 2. 提供数据并封装到DatagramPacket
byte[] buf = "abcdefg".getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length,
InetAddress.getByName("192.168.1.5"), 10000);
// 3. 通过DatagramSocket发送数据包
ds.send(dp);
// 4. 关闭资源
ds.close();
}
}
UDP接收端:
class UdpReceive {
public static void main(String[] args) throws Exception {
// 1. 定义DatagramSocket服务,绑定监听端口10000
DatagramSocket ds = new DatagramSocket(10000);
// 2. 定义数据缓冲区
byte[] buf = new byte[1024];
// 3. 定义DatagramPacket接收数据
DatagramPacket dp = new DatagramPacket(buf, buf.length);
// 4. 接收数据包
ds.receive(dp);
// 5. 提取数据
String data = new String(dp.getData(), 0, dp.getLength());
System.out.println(data);
// 6. 关闭资源
ds.close();
}
}
UDP应用场景
UDP具有TCP无法比拟的速度优势:
- 优势:不属于连接性协议,消耗资源小,处理速度快
- 适用场景:
- 音频视频传输
- 网络直播
- 视频聊天
- 在线游戏
- DNS查询
- 特点:即使丢失少量数据包,也不会对接收结果产生太大影响
五、常见异常处理
网络编程常见异常类型:
| 异常类型 | 描述 | 常见原因 |
|---|---|---|
| java.net.UnknownHostException | 未知主机异常 | 主机名无法解析、DNS配置问题 |
| java.net.SocketException | 套接字异常 | 网络连接问题、端口被占用 |
| java.io.IOException | IO异常 | 输入输出流操作失败 |
| java.net.BindException | 绑定端口异常 | 端口已被占用、权限不足 |
异常处理最佳实践:
try {
// 网络操作代码
Socket socket = new Socket("127.0.0.1", 8080);
// 其他操作...
} catch (UnknownHostException e) {
System.err.println("无法解析主机: " + e.getMessage());
} catch (IOException e) {
System.err.println("网络IO异常: " + e.getMessage());
} catch (Exception e) {
System.err.println("其他异常: " + e.getMessage());
} finally {
// 确保资源被正确关闭
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结
TCP vs UDP对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接 | 无连接 |
| 可靠性 | 可靠传输 | 不保证可靠 |
| 速度 | 相对较慢 | 快速 |
| 开销 | 较大(20字节头部) | 较小(8字节头部) |
| 应用场景 | 文件传输、网页浏览、邮件 | 视频直播、在线游戏、DNS |
| 数据格式 | 字节流 | 数据报 |
Java网络编程要点
- TCP编程:使用Socket和ServerSocket,需要处理流操作
- UDP编程:使用DatagramSocket和DatagramPacket,操作数据包
- 异常处理:网络编程必须妥善处理各种网络异常
- 资源管理:及时关闭Socket连接和流对象,避免资源泄露
- 线程安全:多客户端场景下需要考虑线程安全问题
通过理解TCP和UDP的特性差异,选择合适的协议来满足不同的应用需求,是网络编程的关键。