Java网络编程
UDP和TCP网络协议,基于Socket的UDP和TCP网络编程的介绍。
Author: Msuenb
Date: 2023-02-21
网络基础知识
每个计算设备上都有若干个网卡,每个网卡上有(全球唯一)单独的硬件地址:MAC地址,全称是Media Access Control Address,介质访问控制地址。
IP地址
每个网卡/机器都有一个或多个IP地址,在计算机网络中,依靠IP地址来定义一台计算机的位置。IP地址有两个版本:
- IPV4:如,192.168.0.100,每段从0 - 255。
- IPV6:128bit长,分成8端,每段4个16进制数,全是0的字段可以省略,如,FE::::AAAA::00C2:0002
查询当前机器联网后的IP地址:可以用ipconfig(Windows)、ifconfig(Mac/Linux)查询
保留IP:127.0.0.1 指本机
port端口
每台计算机有 0~65535 个端口,其中 0~1023 这1024个端口,OS已经占用了,操作系统会利用这些端口做一些特殊的事情。如,80是Web服务,23是telnet。剩下的端口 1024~65535 是一般程序可以使用(谨防冲突)。
两台机器通讯就是建立在IP+Port上进行的,数据从一个IP的port出发(发送方),运输到另一个IP的port(接收方)。
在Windows、Linux、Mac上都可以通过netstat -an来查询当前机器和和其他机器同讯时的 IP 和 port。
通信协议
-
UDP: User Datagram Protocol
用户数据报协议,面向无连接的协议,它不保证可靠的数据传输。但是它的速度比较快,而且对可以在较差的网络下使用。
-
TCP: Transmission Control Protocol
传输控制协议,面向连接的协议,它可以做到两台机器的可靠的、无差错的数据传输。但是它对网络的要求比较高,网络必须持续稳定,而且还要保证速度还可以。
UDP网络编程
UDP是一个无连接、无状态的通讯协议,具体表现是:发送方发送消息,如果接受方刚好在目的地,则可以接受;如果接收方不在目的在,则这个消息就会丢失。发送方也无法得知消息是否发送成功。UDP的好处就是简单、节省、经济。
在Java里面主要靠以下这两个类来实现UDP通信:
- DatagramSocket:通讯数据管道,需要绑定在某个IP和Port
- DatagramPacket:集装箱,用来封装数据,上面会有一个地址标签:目的地IP+Port
发送端示例代码:
import java.net.*;
public class UDPSend {
public static void main(String[] args) throws Exception {
// 1. 建立发送端的DatagramSocket
DatagramSocket ds = new DatagramSocket();
// 接受方的ip 和端口号
InetAddress ip = InetAddress.getByName("127.0.0.1");
int port = 9001;
byte[] bytes = "hello, world!".getBytes();
// 2. 建立数据包
DatagramPacket dp = new DatagramPacket(bytes, 0, bytes.length, ip, port);
// 3. 发送数据
ds.send(dp);
// 4. 关闭资源
ds.close();
}
}
接收端示例代码:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPReceive {
public static void main(String[] args) throws Exception{
// 1. 建立接受端DatagramSocket 并指定端口号
DatagramSocket ds = new DatagramSocket(9001);
byte[] buf = new byte[1024];
// 2. 创建数据包
DatagramPacket dp = new DatagramPacket(buf, buf.length);
// 3. 接受数据 若没有收到消息 一直阻塞
ds.receive(dp);
System.out.println(new String(dp.getData(), 0, dp.getLength()));
// 4. 关闭资源
ds.close();
}
}
TCP网络编程
TCP: Transmission Control Protocol,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,它能够做到两台机器之间进行可靠的、无差错的数据传输。
TCP协议会采用三次握手
方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕TCP协议会采用四次挥手
方式断开连接。
单客户端与服务器单次通信
需求:客户端连接服务器,连接成功后给服务发送“hello”,服务器收到消息后,给客户端返回“欢迎登录”,客户端接收消息后,断开连接。
TCP服务器示例代码
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception{
// 1. 创建一个ServerSocket对象 绑定9999端口
ServerSocket server = new ServerSocket(9999);
System.out.println("等待连接...");
// 2. 监听客户端的连接请求 没有则阻塞
Socket socket = server.accept();
InetAddress inetAddress = socket.getInetAddress();
System.out.println(inetAddress.getHostAddress() + " 客户端连接成功");
// 3. 获取输入流 用来接受客户端发来的消息
InputStream is = socket.getInputStream();
// 接受数据
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
System.out.println(inetAddress.getHostAddress() + ": " + sb);
// 4. 获取输出流 用来发送数据给客户端
OutputStream os = socket.getOutputStream();
// 发送数据
os.write("欢迎登录".getBytes());
os.flush();
// 5. 关闭socket
socket.close();
}
}
TCP客户端示例代码
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception{
// 1.准备Socket 连接服务器 需要指定服务器的IP和Port
Socket socket = new Socket("127.0.0.1", 9999);
// 2. 获取输出流 发送数据给服务器
OutputStream os = socket.getOutputStream();
// 发送数据
os.write("hello".getBytes());
os.flush();
// socket.shutdownOutput(); // 写入 -1 标记流的末尾
// 3. 获取输入流 用来接受服务器发来的信息
InputStream is = socket.getInputStream();
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
System.out.println("服务器:" + sb);
// 4.关闭socket
socket.close();
}
}
多客户端与服务器多次通信
通常情况下,服务器不应该只接受一个客户端请求,而是不断地接受来自客户端的所有请求,不断地调用ServerSocket的accept()方法。
如果服务器端要“同时”处理多个客户端的请求,因此服务器端需要为每一个客户端单独分配一个线程来处理,否则无法实现“同时”。
案例需求:多个客户端连接服务器,并进行多次通信
- 每一个客户端连接成功后,从键盘输入英文单词或中国成语,并发送给服务器
- 服务器收到客户端的消息后,把词语“反转”后返回给客户端
- 客户端接收服务器返回的“词语”,打印显示
- 当客户端输入“stop”时断开与服务器的连接
- 多个客户端可以同时给服务器发送“词语”,服务器可以“同时”处理多个客户端的请求
服务端示例代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// 1.准备一个ServerSocket
ServerSocket server = new ServerSocket(9999);
System.out.println("等待连接...");
int count = 0;
while (true) {
// 2. 监听客户端的连接
Socket socket = server.accept();
System.out.println("第" + ++count + "个客户端: " +
socket.getInetAddress().getHostAddress() + " 连接成功");
ClientHandlerThread ct = new ClientHandlerThread(socket);
ct.start();
}
}
static class ClientHandlerThread extends Thread {
private Socket socket;
private String ip;
public ClientHandlerThread(Socket socket) {
this.socket = socket;
this.ip = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
try {
// 1. 获取输入流 接受客户端发来的信息
BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 2. 获取输出流 用来发送数据给客户端
PrintStream out = new PrintStream(socket.getOutputStream());
String str;
// 3. 不断接受客户端发送的数据
while ((str = input.readLine()) != null) {
// 反转 并返回给客户端
StringBuilder sb = new StringBuilder(str);
sb.reverse();
out.println(sb);
}
System.out.println("客户端" + ip + "正常退出");
} catch (IOException e) {
System.out.println("客户端" + ip + "异常退出");
} finally {
try {
socket.close(); // 4 断开连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端示例代码:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
// 1. 准备Socket 连接到服务器
Socket socket = new Socket("127.0.0.1", 9999);
// 2. 获取输出流 用来发送数据给客户端
PrintStream out = new PrintStream(socket.getOutputStream());
// 3. 获取输入流 用来接受服务端数据
BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("输入发给服务器的单词或成语:");
String msg = scanner.nextLine();
if (msg.equals("quit")) {
socket.shutdownOutput();
break;
}
// 4. 发送数据
out.println(msg);
// 接受数据
String receive = input.readLine();
System.out.println("服务器:" + receive);
}
// 5 关闭socket 断开与服务器连接
scanner.close();
socket.close();
}
}