先赞后看,养成习惯!!! ^ _ ^ ❤️ ❤️ ❤️
码字不易,大家的支持就是我坚持下去的动力,点赞后不要忘记关注我哦
📘 本系列文章为本人在学习路上遇到的问题和解决方法,在这里撰写成文是为了巩固知识和帮助其他友友。
个人主页 🔍: 加瓦糕手
专栏链接 📁: 问题分析简介
如有错误,请您指正批评 ^ _ ^
1. 网络编程基础
为什么需要网络编程?-丰富的网络资源
用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
相比本地资源来说,网络提供了更为丰富的网络资源:

所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。
1.1 什么是网络编程?
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

当然,我们只要满足进程不同就行;即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
但是,我们以定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:
进程A:编程来获取网络资源。
进程B:变成来提供网络资源。
1.2 网络中的基本概念
发送端和接收端
再一次网络传输时:
发送端:数据的发送送进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收收进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
**注意:**发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

请求和响应:
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送。
第二次:响应数据的发送。

客户端和服务端
**服务端:**在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
**客户端:**获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
客户端获取服务资源:

客户端保存资源在服务端:

常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
-
客户端先发送请求到服务端
-
服务端根据请求数据,执行相应的业务处理
-
服务端返回响应:发送业务处理结果
-
客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

2. Socket套接字
2.1 概念
Socket套接字,是系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。(相当于提供一个Socket接口,用户可以通过Socke套接字来实现send、receive数据)。
2.2 分类
Socket套接字主要针对传输层协议划分为如下三类:
**流套接字:**使用传输层TCP协议。
TCP即传输控制协议,传输层协议。
以下为TCP的特点:
有连接、可靠传输、面向字节流、有接收缓冲区、也有发送缓冲区、大小不限。
**数据报套接字:**使用传输层UDP协议。
UDP即用户数据报协议,传输层协议。
以下为UDP的特点:
无连接、不可靠传输、面向数据报、有接收缓冲区、无发送缓冲区、大小受限:一次最多传输64K。
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
**原始套接字:**原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
2.3 Java数据报套接字通信模型
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:

以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:

2.4 Java流套接字通信模型

3. UDP数据报套接字编程
API介绍
DatagramSocket
DatagramSocket是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket构造方法:
|--------------------------|----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)。 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket方法:
|--------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket 是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
|----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[] buf, int length) | 构造⼀个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
| DatagramPacket(byte[] buf, int offset,int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP 和端口号。 |
DatagramPacket 方法:
|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址。 |
| int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从发送的数据报中,获取接收端主机端⼝号。 |
| byte[] getData() | 获取数据报中的数据。 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建。
InetSocketAddress
InetSocketAddress(SocketAddress的子类)构造方法
|----------------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetSocketAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |
3.1 代码示例
UDP Echo Server
java
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Proxy;
import java.net.SocketException;
public class EchoServer {
private DatagramSocket socket;
public EchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//启动服务器,完成主要的业务逻辑
public void start() throws IOException {
System.out.println("服务端启动");
while (true){
// 1. 读取请求并解析
// 1) 创建一个空白的 DatagramPacket 对象
DatagramPacket reqPacket=new DatagramPacket(new byte[4096],4096);
// 2) 通过 receive 读取网卡的数据. 如果网卡没有收到数据, 就会阻塞等待.
socket.receive(reqPacket);
// 3) 把 DatagramPacket 中的数据解析成字符串. 只需要从 DatagramPacket 取到有效的数据即可.
String request=new String(reqPacket.getData(),0,reqPacket.getLength());
//2.计算响应
String response= process(request);
//3.写回到客户端
DatagramPacket resPacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
reqPacket.getSocketAddress());
socket.send(resPacket);
// 4. 打印日志.
System.out.printf("[%s:%d] req: %s, resp: %s\n",
reqPacket.getAddress(), reqPacket.getPort(), request, response);
}
}
//回显服务器
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server=new EchoServer(9090);
server.start();
}
}
UDP Echo Client
java
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class EchoClient {
private DatagramSocket socket=null;
private String serverIp;
private int serverPort;
public EchoClient(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
System.out.println("客户端启动!");
while (true){
System.out.print(">");
String request=scanner.next();
DatagramPacket reqPacket=new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
socket.send(reqPacket);
//3.读取服务器的响应
DatagramPacket respPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(respPacket);
String response=new String(respPacket.getData(),0,respPacket.getLength());
// 4. 把响应显示到控制台.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
EchoClient client=new EchoClient("127.0.0.1",9090);
client.start();
}
}
UDP Dict Server
编写一个英译汉的服务器。只需要重写process。
java
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class DictServer extends EchoServer{
private Map<String,String> dict=new HashMap<>();
public DictServer(int port) throws SocketException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("bird", "小鸟");
dict.put("sheep", "小羊");
dict.put("cow", "小牛");
dict.put("chicken", "小鸡");
dict.put("rabbit", "小兔子");
dict.put("fish", "小鱼");
dict.put("wolf", "狼");
dict.put("monkey", "猴子");
dict.put("chicken", "小鸡");
dict.put("fish", "小鱼");
dict.put("tiger", "老虎");
dict.put("lion", "狮子");
dict.put("wolf", "狼");
dict.put("monkey", "猴子");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"该单词没有查到");
}
public static void main(String[] args) throws IOException {
DictServer server=new DictServer(9090);
server.start();
}
}
4. TCP流套接字编程
API介绍
ServerSocket
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
|------------------------|---------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
|-----------------|-------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该 Socket建立与客户端的连接,否则阻塞等待。 |
| void close() | 关闭此套接字。 |
Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服 务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
|------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。 |
Socket 方法:
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
4.1 代码示例
TCP Echo Server
java
package network.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class EchoServer {
private ServerSocket serverSocket;
public EchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
Socket socket=serverSocket.accept();
processConnection(socket);
}
private void processConnection(Socket socket){
System.out.printf("[%s:%d] 服务器上线!\n", socket.getInetAddress().toString(), socket.getPort());
try (InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
// 实现通信的代码.
// 一个客户端可能会和服务器有多轮的请求响应交互.
Scanner scanner=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while (true){
if (!scanner.hasNext()){
// 针对客户端下线逻辑的处理. 如果客户端断开连接了 (比如客户端结束了)
// 此时 hasNext 就会返回 false
// 如果是使用 read 方法, 就会出现返回 -1 的情况. 也可以用来判定客户端断开连接.
System.out.printf("[%s:%d] client offline!\n", socket.getInetAddress().toString(), socket.getPort());
break;
}
String request=scanner.next();
String response=process(request);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n", socket.getInetAddress().toString(), socket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server=new EchoServer(9090);
server.start();
}
}
TCP Echo Client
java
package network.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class EchoClient {
private Socket socket;
public EchoClient(String serverIp,int serverPort) throws IOException {
// 在 new 这个对象的时候就涉及到 "建立连接操作"
// 由于连接建立好了之后, 服务器的信息就在操作系统中被 TCP 协议记录了. 我们在应用程序层面上就不需要保存 IP 和 端口.
socket = new Socket(serverIp, serverPort);
}
public void start(){
System.out.println("客户端启动");
Scanner scanner=new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
while (true){
// 1. 从控制台读取用户的输入.
System.out.print("> ");
String request=scanner.next();
writer.println(request);
writer.flush();
if (!scannerNet.hasNext()) {
System.out.println("server disconnect");
break;
}
String response=scannerNet.next();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
EchoClient client=new EchoClient("127.0.0.1",9090);
client.start();
}
}
5. 服务器引入多线程
如果只是单个线程,无法同时响应多个客户端。
此处给每个客户端都分配一个线程。
java
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
Socket socket=serverSocket.accept();
Thread t=new Thread(()->{
processConnection(socket);
});
t.start();
}
}
5.1 服务器引入线程池
java
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
// 使⽤线程池, 来解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
6. 长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。