前言
在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”
博客主页:KC老衲爱尼姑的博客主页 博主的github,平常所写代码皆在于此 共勉:talk is cheap, show me the code 作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
网络编程
为什么需要网编程?
&ebsp;&ebsp;当我们使用浏览器进行搜索时,浏览器会根据关键字搜索出视频,图片文本等资源,这些资源都属于网络资源。网络资源相比于本地资源来说更加的丰富多彩。而这些网络资源都需要通过网络编程来进行数据传输。
什么是网络编程
&ebsp;&ebsp;网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络数据传输。但是,同一台主机上的不同进程,如果是基于网络来进行通信,也属于网络编程。
网络编程中的基本概念
发送端和接收端
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送,告诉服务端我要获取xx资源。
第二次:响应数据的发送,服务端返回给客户端xx资源。
举个栗子,这就好比,在外面吃饭,你跟老板说来一份青椒炒肉,自然就会给你提供一份青椒炒肉。
客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务,就像我们平时用的B站app上面的视频,图片等资源,都是通过网络从服务器上得到数据,然后通过网络传输到app上。
客户端:获取服务的一方进程,称为客户端。简单来说就是各种软件。
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源在服务端
举个栗子, 银行提供了存款服务,用户(客户端)把钱(资源)存在银行(服务端)。同时银行提供了取款服务,用户可以去银行取钱(获得服务端资源)。
常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
Socket套接字
概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
套接字分类
- 流套接字:使用传输层TCP协议,对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
- 数据报套接字:使用传输层UDP协议,对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
- 原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
Java数据报套接字通信模型
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
Java流套接字通信模型
UDP数据报套接字编程
DatagramSocket API
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 API
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组 |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组。address指定目的主机的IP和端口号 |
DatagramPacket 方法
InetAddress | |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端的ip |
int getPort() | 接收端主机IP地址从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端的主机和端口号 |
byte[] getData() | 取接收端主机端口号获取数据报中的数据 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
UDP客户端服务器回显服务程序
回显程序就是客户端向服务端发送什么,服务端就返回给客户端什么,就像在夜深人静的时候,你突然说了一句话,然后无形你听到了一句一模一样你刚说的话。
客户端代码设计
- .创建客户端DatagramSocket
- 准备要发送的数据
- .组装要发送的UDP数据报,包含数据,及发送的服务端信息
- 发送UDP数据报
代码示例
import java.io.IOException; import java.net.*; import java.util.Scanner;
public class UdpEchoClient {
/**
/
private DatagramSocket clientSocket;
/*
* 服务器IP地址
/
private String serverIp;
/*
* 服务器端口号
*/
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
//客户端可以自己指定端口号,也可以让系统自动分配,但是自己指定的端口号可能已经被使用了,所以系统分配端口号更好
this.clientSocket = new DatagramSocket();
this.serverIp = ip;
this.serverPort = port;
}/** * 启动客户端 * @throws IOException */ public void start() throws IOException { //1. 获取用户输入的数据 Scanner sc = new Scanner(System.in); while (true) { System.out.print("请输入需要发送的数据->"); String request = sc.next(); //2. 根据用户输入的数据,将数据打包,待发送 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort); // 3.发送数据 clientSocket.send(requestPacket); // 4.接收请求 DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024); clientSocket.receive(responsePacket); String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8"); System.out.printf("我的请求: %s, 它的回应: %s\n", request, response); } } public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090); client.start(); }
}
服务端代码设计
- 创建服务端DatagramSocket
- 创建数据报,用于接收客户端发送的数据
- 等待客户端数据,一旦得到构造数据报
- 返回数据
代码示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {
/**
* 准备好socket实例,准备传输
*/
private DatagramSocket serverSocket;/** *指定端口 * @param port * @throws SocketException */ public UdpEchoServer(int port) throws SocketException { this.serverSocket = new DatagramSocket(port); } /** * 启动服务器 * @throws IOException */ public void start() throws IOException { System.out.println("服务器准备就绪!"); while (true) { //1. 读取客户端的请求 DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);//参数为储存数据的数组与最大空间大小 serverSocket.receive(requestPacket); //2. 解析收到的数据包,一般解析成字符串进行处理 String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8"); //3. 处理请求 String response = process(request); //4. 发送请求,因为数据的传输是依据DatagramPacket来进行传输的,所以我们需要先包装在发送 //除此之外,我们还需要知道客户端的地址和端口号 //接收DatagramPacket对象时。该对象里面存有客户端的地址和端口号。可以使用getSocketAddress方法获取 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); serverSocket.send(responsePacket); //5. 输出发送日志 System.out.printf("[%s:%d] 收到的请求: %s, 回应: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); } } /** * .处理数据,回显服务直接将原数据返回即可 * @param data * @return */ public String process(String data) { return data; } public static void main(String[] args) throws IOException { UdpEchoServer udpEchoServer = new UdpEchoServer(9090); udpEchoServer.start(); }
}
运行结果:
UDP客户端服务器简单翻译服务程序
翻译其实就是利用了哈希表,我们只需要将中文中的词和英文中相对应意思的词一一对应,全部存放在哈希表中,这样就 构成了一个词库。当需要将中文翻译好成英文的时候,就只需要在哈希表中查找对应的英文。对应该程序我们只需将上述的服务端代码中的处理请求的部分稍加修改就行。
翻译服务端代码
import java.io.IOException; import java.net.SocketException; import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer{
//最简单的翻译处理服务器
private final HashMap<String, String> dict = new HashMap<>();public UdpDictServer(int port) throws SocketException { super(port); dict.put("cat", "小猫"); dict.put("dog", "小狗"); dict.put("bird", "小鸟"); } @Override public String process(String data) { return dict.getOrDefault(data, "词库没有该单词!"); } public static void main(String[] args) throws IOException { UdpDictServer server = new UdpDictServer(9090); server.start(); }
}
运行结果:
TCP流套接字编程
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP客户端服务器回显服务程序
TCP和UDP不同,TCP是需要建立连接,并且通过对文件读写的方式以字节为单位进行传输。对于TCP传输,java 中提供了2个类来进行数据的传输,一个是ServerSocket,用于服务端接受客户端的连接,另一个是Socket,用于服务端和客户端之间的通信。
TCP客户端服务器简单翻译服务程序
客户端程序设计:
- 创建Socket对象
- 初始化服务端的ip和端口
- 启动客户端,发送数据并接收返回数据
示例代码
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner;
public class TcpEchoClient {
/**
* 创建Socket对象
*/
private Socket socket;/** * * @param serverIP 服务端ip * @param serverPort 服务端口 * @throws IOException */ public TcpEchoClient(String serverIP, int serverPort) throws IOException { socket = new Socket(serverIP, serverPort); } /** * 启动客户端 */ public void start() { System.out.println("客户端启动成功!"); //用户输入数据 Scanner input = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream()) { try (OutputStream outputStream = socket.getOutputStream()) { while (true) { //请输入数据 System.out.print("请输入需要传输的数据!->"); String request = input.next(); //发送数据 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request); //刷新缓冲区 printWriter.flush(); //接收回应 Scanner receiverScanner = new Scanner(inputStream); String response = receiverScanner.next(); //输出数据 System.out.printf("我的请求:%s 它的回应:%s\n", request, response); } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9092); client.start(); }
}
服务端设计:
- 创建socket对象,初始化端口
- 接收客户端的数据,返回客户端的数据。
示例代码
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 TcpEchoServer {
/**
* 创建socket对象
/
private ServerSocket serverSocket;
/*
* 初始化服务端端口
*/
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}/** * 启动服务器 * @throws IOException */ public void start() throws IOException { System.out.println("服务器准备就绪!"); while (true) { // 3. 接收客户端的数据 Socket clientSocket = serverSocket.accept(); // 4. 接收 处理 回应数据 processContain(clientSocket); } } private void processContain(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 服务器正式与客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream()) { try (OutputStream outputStream = clientSocket.getOutputStream()) { //接收数据 使用Scanner比InputStream的原生方法read更方便 Scanner receiveScanner = new Scanner(inputStream); while (true) { if (!receiveScanner.hasNext()) { System.out.printf("[%s:%d] 服务器与客户端已经断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request = receiveScanner.next(); //处理数据 String response = process(request); //发送数据,为了方便,我们可以使用PrintWriter类将OutputStream类对象包裹起来,就是用来把数据打印到文件里面 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); //及时刷新缓冲区 printWriter.flush(); //输出回应信息 System.out.printf("[%s:%d] 收到的请求: %s 回应: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } } }catch (IOException e) { e.printStackTrace(); } finally { //释放资源 相当于挂断电话 clientSocket.close(); } } public String process(String data) { return data; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9092); server.start(); }
}
运行结果:
上述服务端代码无法处理多个客户端的请求,因为每次建立连接服务端只能和一个客户端连接,当服务端和客户端建立连接后,处理数据会进入processContain方法,如果此时又有一个客户端尝试建立连接发送数据,就无法跳出processContain中的循环去建立新的连接,导致客户端和服务端连接不上,为了解决上述问题,可以使用线程,每次建立连接就分配一个线程去处理该连接。
线程版服务端代码
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 TcpEchoServer {
/**
* 创建socket对象
/
private ServerSocket serverSocket;
/*
* 初始化服务端端口
*/
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}/** * 启动服务器 * @throws IOException */ public void start() throws IOException { System.out.println("服务器准备就绪!"); while (true) { // 3. 接收客户端的数据 Socket clientSocket = serverSocket.accept(); // 4. 接收 处理 回应数据 Thread t= new Thread(() -> { try { processContain(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } } private void processContain(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 服务器正式与客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream()) { try (OutputStream outputStream = clientSocket.getOutputStream()) { //接收数据 使用Scanner比InputStream的原生方法read更方便 Scanner receiveScanner = new Scanner(inputStream); while (true) { if (!receiveScanner.hasNext()) { System.out.printf("[%s:%d] 服务器与客户端已经断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request = receiveScanner.next(); //处理数据 String response = process(request); //发送数据,为了方便,我们可以使用PrintWriter类将OutputStream类对象包裹起来,就是用来把数据打印到文件里面 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); //及时刷新缓冲区 printWriter.flush(); //输出回应信息 System.out.printf("[%s:%d] 收到的请求: %s 回应: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } } }catch (IOException e) { e.printStackTrace(); } finally { //释放资源 相当于挂断电话 clientSocket.close(); } } public String process(String data) { return data; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9092); server.start(); }
}
运行结果:
server.png)]TCP客户端服务器简单翻译服务程序
该逻辑和UDP实现一致
示例代码
import java.io.IOException;
import java.util.HashMap;public class TcpDictServer extends TcpEchoServer{
private final HashMap<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
}@Override public String process(String data) { return dict.getOrDefault(data, "词库为找到该单词!"); } public static void main(String[] args) throws IOException { TcpDictServer server = new TcpDictServer(9092); server.start(); }
}
运行结果:
TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
- 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等.
一发一收(短连接)
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只有客户端请求,但没有服务端响应的示例:
TCP服务端
示例代码
import java.io.*; import java.net.ServerSocket; import java.net.Socket;
public class TcpServer {
//服务器socket要绑定固定的端口
private static final int PORT = 8888;public static void main(String[] args) throws IOException { // 1.创建一个服务端ServerSocket,用于收发TCP报文 ServerSocket server = new ServerSocket(PORT); // 不停的等待客户端连接 while (true) { System.out.println("------------------------------------------------ ---"); System.out.println("等待客户端建立TCP连接..."); // 2.等待客户端连接,注意该方法为阻塞方法 Socket client = server.accept(); System.out.printf("客户端IP:%s%n", client.getInetAddress().getHostAddress()); System.out.printf("客户端端口号:%s%n", client.getPort()); // 5.接收客户端的数据,需要从客户端Socket中的输入流获取 System.out.println("接收到客户端请求:"); InputStream is = client.getInputStream(); // 为了方便获取字符串内容,可以将以上字节流包装为字符流 BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; // 一直读取到流结束:TCP是基于流的数据传输,一定要客户端关闭Socket输出流才表示服务端接收IO输入流结束 while ((line = br.readLine()) != null) { System.out.println(line); } // 6.双方关闭连接:服务端是关闭客户端socket连接 client.close(); } }
}
如果没有客户端请求,此时代码是阻塞等待在 server.accept() 代码行,直到有新的客户端申请建立连接。
TCP客户端
示例代码
import java.io.*;
import java.net.Socket;public class TcpClient {
//服务端IP或域名
private static final String SERVER_HOST = "localhost";
//服务端Socket进程的端口号
private static final int SERVER_PORT = 8888;public static void main(String[] args) throws IOException { // 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 Socket client = new Socket(SERVER_HOST, SERVER_PORT); // 4.发送TCP数据,是通过socket中的输出流进行发送 OutputStream os = client.getOutputStream(); // 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流 PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8")); // 4-1.发送数据: pw.println("hello world!"); // 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区 pw.flush(); // 7.双方关闭连接:客户端关闭socket连接 client.close(); }
}
运行结果:
客户端向服务端发送数据后就会重新断开连接,而服务端不会断开会等待下一个客户端的连接。