网络编程基础
网络资源
// 所谓的网络资源, 其实就是在网络中可以获取的各种数据资源, 而所以得网络资源,都是通过网络编程来实现数据传输的
网络编程
// 网络编程, 指网络上的主机, 通过不同的进程, 以编程的方式实现网络通信(网络数据传输)
// 网络编程 就是写一个应用程序, 让这个程序可以使用网络通信, 这里就需要调用传输层提供的api
1. 基本概念
1.1 发送端和接收端
// 在一次网络数据传输时:
1.1.1 发送端: 数据的发送方进程, 称为发送端. 发送端主机即网络通信中的源主机
1.1.2 接收端: 数据的接收方进程, 称为接收端. 接收端主机即网络通信中的目的主机
1.1.3 收发端: 发送端和接收端两端, 也简称为收发端
// 发送端和接收端只是相对的, 只是一次网络数据传输产生数据流向后的概念
1.2 请求和响应
// 一般来说, 获取一个网络资源, 设计到两次网络数据传输:
1.2.1 第一次: 请求数据的发送
1.2.2 第二次: 响应数据的发送
1.3 客户端和服务端
1.3.1 服务端: 在常见的网络数据传输场景下, 把提供服务的一方进程, 称为服务端, 可以提供对外服务
1.3.2 获取服务的一方进程, 称为客户端
// 对于服务来说, 一般提供:
// 客户端获取服务资源
// 客户端保存资源在服务端
1.4 常见的客户端
// 最常见的场景, 客户端是指给用户使用的程序, 服务端是提供用户服务的程序:
1.4.1 客户端先发送请求到服务端
1.4.2 服务端根据请求数据, 执行相应的业务处理
1.4.3 服务端返回响应: 发送业务处理结果
1.4.4 客户端根据响应数据, 展示处理结果
UDP 数据报套接字
API 介绍 (socket api)
// 两个核心类
1. DatagramSocket
// 是一个 Socket 对象
// 操作系统, 通过使用文件这样的概念, 来管理一些软硬件资源
// Java 中的 socket 对象, 就对应着系统里的 socket 文件(最终还是要落到网卡)
// 要进行网络通信, 必须得先有 socket 对象
// DatagramSocket 构造方法:
|--------------------------|--------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个 UDP 数据报套接字的 Socket, 绑定到本机任意一个随机端口 (一般用于客户端) |
| DatagramSocket(int port) | 创建出一个 UDP 数据报套接字的 Socket, 绑定到本机指定的端口 (一般用于服务端) |
// DatagramSocket 方法:
|--------------------------------|------------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报 (如果 没有接收的数据报, 该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包 (不会阻塞等待, 直接发送) |
| void close() | 关闭此数据报套接字 |
// 客户端使用哪个端口, 系统自动分配
// 服务器使用哪个端口, 手动指定的
2. DatagramPacket
// 表示了一个 UDP 数据报
// 代表了系统中设定的 UDP 数据报的二进制结构
代码实现 UDP 客户端服务器
1. UDP 的 回显服务器
// 回显服务器: 请求和响应是一样的
// 配合回显客户端使用
java
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 1. 读取请求, 并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把响应写会客户端
DatagramPacket datagramPacket = new DatagramPacket(response.getBytes(),
response.getBytes().length, requestPacket.getSocketAddress());
socket.send(datagramPacket);
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(9090);
server.start();
}
}

2. UDP 的 回显客户端
java
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 服务器的 IP 和 服务器的端口
public UdpEchoClient(String ip, int port) throws SocketException {
serverIp = ip;
serverPort = port;
// 这个 new 操作, 就不再指定端口了, 让系统自动分配一个空闲端口
socket = new DatagramSocket();
}
// 让这个客户端反复的从控制台读取用户输入的内容, 把这个内容构造成 UDP 请求,
// 发给服务器, 再读取服务器返回的 UDP 响应
// 最终再显示在客户端的屏幕上
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
while (true) {
// 1. 从控制台读取用户输入的内容
System.out.print("-> ");
String request = scanner.next();
// 2. 构造请求对象, 并发给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
// 3. 读取服务器的响应, 并解析出响应内容
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive((responsePacket));
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 显示到屏幕上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}

3. 翻译服务器
// 翻译服务器的请求是一些英文单词, 响应则是对应的中文翻译
// 因为我们的翻译服务器和之前的回显服务器很多逻辑都很相似, 所以直接继承 (复用)
java
public class UdpDictServer extends UdpEchoServer{
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("pig","小猪");
dict.put("fuck","卧槽");
}
// 是要复用之前的代码, 但是又要做出一些调整
public String process(String request) {
// 把请求对应单词的翻译给返回回去
return dict.getOrDefault(request, "该词没有查询到");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}

TCP 数据报套接字
// TCP 分量要比 UDP 更重, 用更多协议
// 字节流, 一个字节一个字节进行传输的, 一个 TCP 数据报, 就是一个 字节数组 byte[]
API 介绍
1. ServerSocket
// 给服务请求使用的 socket
2. Socket
// 既会给服务器使用, 也会给客户端使用
代码实现 TCP 版本的客户端服务器
1. TCP 回显服务器
java
public class TcpEchoServer {
private ServerSocket serverSocket = null;
private ExecutorService service = Executors.newCachedThreadPool();
// 这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
// 通过这个方法来处理一个连接的逻辑
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
// 接下来就可以读取请求, 根据请求计算响应,返回响应三步走了
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream =clientSocket.getOutputStream()) {
while (true) {
// 1. 读取请求并解析, 为了方便, 直接使用 Scanner
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读取完毕, 客户端下线
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割 (比如换行这种)
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回个客户端
PrintWriter writer = new PrintWriter(outputStream);
// 使用 PrintWriter 的 println 方法, 把响应返回给客户端
// 用 println, 而不用 print, 就是为了在结尾加一个 \n, 方便客户端读取响应, 使用 scanner.next 读取
writer.println(response);
// 这里还需要加入一个 "刷新缓冲区" 操作
writer.flush();
// 日志, 打印当前的请求详情
System.out.printf("[%s:%d] req: %s, resp: %s\n ", clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}

2. TCP 回显客户端
java
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.printf("客户端启动!\n");
Scanner scannerConsole = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while (true) {
// 1. 从控制台输入字符串
System.out.printf("->");
String request = scannerConsole.next();
// 2. 把请求发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
// 3. 从服务器读取响应
Scanner scannerNetwork = new Scanner(inputStream);
String response = scannerNetwork.next();
// 4. 把响应打印出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
UDP 和 TCP 特点对比
// UDP : 无连接, 不可靠传输, 面向数据报, 全双工
// TCP : 有连接, 可靠传输, 面向字节流, 全双工