一、网络编程
1.网络编程基础
(1).什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)
(2).网络编程中的基本概念
(1)发送端和接收端
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端
注意:发送端和接收端只是相对的。
(2)请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送。
第二次:响应数据的发送。
(3)客户端与服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端
(4)常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
1. 客户端先发送请求到服务端
2. 服务端根据请求数据,执行相应的业务处理
3. 服务端返回响应:发送业务处理结果
4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
2.Socket套接字
(1)概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基 于Socket套接字的网络程序开发就是网络编程。是应用层与传输层之间的桥梁,是程序的入口
(2)分类
流套接字:使用传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
以下为TCP的特点:
有连接(有连接:类似打电话,需要先接通,才可以讲话,进行沟通(数据通信))
可靠传输(传输的过程中,数据的发送方,知道这个数据有没有发送成功,有没有手袋数据,打电话就是可靠传输)
面向字节流(以字节为单位,进行数据传输(类似于文件的字节流)TCP可以一次接收1个字节或10个,或20个字节。类似于发快递—发床,床可以拆分发好几个快递,一个快递是床头,一个快递是床板—TCP)
有接收缓冲区,也有发送缓冲区(全双工)
大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情 况下,是无边界的数据,可以多次发送,也可以分开多次接收
数据报套接字::使用传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
以下为UDP的特点:
无连接(无连接:类似发短信,不需要打通,只需要知道电话号码就可以进行发送数据(电话号码存在))
不可靠传输(传输的过程中,数据发送方不知道数据有没有成功到达,或者接收方有没有收到数据。微信,短信,QQ都是不可靠传输。发完之后,也不知道对方是否收到了)
面向数据报(以数据报为单位,进行数据传输。UDP一次发送/接收必须是完整的数据报,不能是半个/一个半。类似于发快递,发电视机,电视机不能拆分,只能发送一个完整的电视机UDP)
有接收缓冲区,无发送缓冲区(全双工:一条链路,双向通路。A和B可以同时互相发消息,两个方向同时传输,A既能发又能收,B也是)
大小受限:一次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一 次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
3.UDP数据报套接字编程
(1)DatagramSocket API (数据报套接字 API)
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
DatagramSocket 方法:
(2)DatagramPacket API (数据包 API)
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
DatagramPacket 方法:
(4)示例一:一发一收(无响应)
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只 有客户端请求,但没有服务端响应的示例:
UDP服务端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* 服务端
*/
public class UDPServer {
//1. 创建套接字
//2. 接收数据
//3. 处理数据
//4. 显示数据
public DatagramSocket socket = null;
//服务端, 需要指定端口号
public UDPServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务端已启动, 等待接收数据...");
/**
* void receive(DatagramPacket p)
* DatagramPacket: 数据报
*/
/**
* DatagramPacket(byte buf[], int length)
* 参数说明
* buf: 接收数据的数组, 类似于食堂打饭的餐盘
* length: 接收数据的长度
*/
while (true) {
byte buf[] = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,buf.length);
socket.receive(packet);
//处理数据
String request = new String(packet.getData(),0,packet.getLength(),
"UTF-8");
System.out.printf("接收到客户端的数据:[%s:%d]:%s\n",packet.getAddress().getHostName(),
packet.getPort(),request);
}
}
public static void main(String[] args) throws IOException {
UDPServer server = new UDPServer(9090);
server.start();
}
}
运行后,服务端就启动了,控制台输出如下:
可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报。
UDP客户端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* 客户端
* <p>
* 站在客户端的角度, 五元组
* 源IP: 自己
* 源端口号: 系统分配
* 目的IP: 服务器的IP, 127.0.0.1
* 目的端口: 服务器的端口,9090
* 协议: UDP
*/
public class UDPClient {
//1. 创建socket
//2. 发送数据
DatagramSocket socket = null;
String serverIp;
int serverPort;
public UDPClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();// 不指定端口号, 系统随机分配
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
//发送数据
System.out.println("客户端已启动...");
Scanner scanner = new Scanner(System.in);
/**
* void send(DatagramPacket p)
*
* DatagramPacket 发送的数据报, 数据报中要指明目的IP和端口
*
* DatagramPacket(byte buf[], int offset, int length,
* InetAddress address, int port)
* 参数说明:
* buf: 发送的数据的byte数组
* offset: 从数组的某一位开始发送
* length: 发送的数据长度
* address: IP
* port: 端口号
*/
while (true){
System.out.println("请输入要发送的内容:");
String request = scanner.nextLine();//从控制台接收一个数据
DatagramPacket packet = new DatagramPacket(request.getBytes(),0,
request.getBytes().length, InetAddress.getByName(serverIp),serverPort );
socket.send(packet);
}
}
public static void main(String[] args) throws IOException {
UDPClient client = new UDPClient("127.0.0.1",9090);
client.start();
}
}
客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下
在客户端输入内容:
服务端响应接收到传输的内容:
从以上可以看出,发送的UDP数据报(假设发送的数据字节数组长度为M),在接收到以后(假设接收 的数据字节数组长度为N):
1. 如果N>M,则接收的byte[]字节数组中会有很多初始化byte[]的初始值0,转换为字符串就是空白 字符;
2. 如果N<M,则会发生数据部分丢失(可以自己尝试,把接收的字节数组长度指定为比发送的字节数组长度更短)。
要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长度。
(5)示例二:请求响应
客户端发送请求,服务端要进行响应,客户端根据响应进行处理。比如简单的字典翻译服务,服务端根据客户端发送的请求,进行翻译,并把翻译的结果返回给客户端。
UDP服务端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
/**
* 服务端
*/
public class UDPDictServer {
//1. 创建套接字
//2. 接收数据
//3. 处理数据
//4. 显示数据
public DatagramSocket socket = null;
public HashMap<String,String> dictMap = new HashMap<>();
//服务端, 需要指定端口号
public UDPDictServer(int port) throws SocketException {
socket = new DatagramSocket(port);
dictMap.put("cat","猫");
dictMap.put("dog","狗");
dictMap.put("pig","猪");
dictMap.put("fox","狐狸");
dictMap.put("frog","青蛙");
}
public void start() throws IOException {
System.out.println("服务端已启动, 等待接收数据...");
/**
* void receive(DatagramPacket p)
* DatagramPacket: 数据报
*/
/**
* DatagramPacket(byte buf[], int length)
* 参数说明
* buf: 接收数据的数组, 类似于食堂打饭的餐盘
* length: 接收数据的长度
*/
while (true) {
byte buf[] = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,buf.length);
socket.receive(packet);
//处理数据
String request = new String(packet.getData(),0,packet.getLength(),
"UTF-8");
// System.out.printf("接收到客户端的数据:[%s:%d]:%s\n",packet.getAddress().getHostName(),
// packet.getPort(),request);
String response = process(request);
//发送给客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,packet.getSocketAddress());
socket.send(responsePacket);
System.out.printf("接收到客户端的数据:[%s:%d]:%s, 返回数据:%s\n",packet.getAddress().getHostName(),
packet.getPort(),request,response);
}
}
private String process(String request) {
return dictMap.getOrDefault(request,"无法翻译");
}
public static void main(String[] args) throws IOException {
UDPDictServer server = new UDPDictServer(9090);
server.start();
}
}
服务端启动:
UDP客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
/**
* 客户端
* <p>
* 站在客户端的角度, 五元组
* 源IP: 自己
* 源端口号: 系统分配
* 目的IP: 服务器的IP, 127.0.0.1
* 目的端口: 服务器的端口,9090
* 协议: UDP
*/
public class UDPDictClient {
//1. 创建socket
//2. 发送数据
DatagramSocket socket = null;
String serverIp;
int serverPort;
public UDPDictClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();// 不指定端口号, 系统随机分配
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
//发送数据
System.out.println("客户端已启动...");
Scanner scanner = new Scanner(System.in);
/**
* void send(DatagramPacket p)
*
* DatagramPacket 发送的数据报, 数据报中要指明目的IP和端口
*
* DatagramPacket(byte buf[], int offset, int length,
* InetAddress address, int port)
* 参数说明:
* buf: 发送的数据的byte数组
* offset: 从数组的某一位开始发送
* length: 发送的数据长度
* address: IP
* port: 端口号
*/
while (true){
System.out.println("请输入要发送的内容:");
String request = scanner.nextLine();//从控制台接收一个数据
DatagramPacket packet = new DatagramPacket(request.getBytes(),0,
request.getBytes().length, InetAddress.getByName(serverIp),serverPort );
socket.send(packet);
//接收响应
byte[] buf = new byte[1024];
DatagramPacket responsePacket = new DatagramPacket(buf,buf.length);
socket.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 {
UDPDictClient client = new UDPDictClient("127.0.0.1",9090);
client.start();
}
}
客户端启动:
在客户端输入要翻译的英语单词:
服务端接收到数据后,返回翻译结果:
4.TCP流套接字编程
(1)ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。(客户端不用)
ServerSocket 构造方法:
ServerSocket 方法:
(2)Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据 的。
Socket 构造方法:
Socket 方法:
(3)TCP中的长短链接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数 据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以 多次收发数据。
对比以上长短连接,两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的 消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的 处理请求。
(4)示例一:一发一收(短连接)
以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只 有客户端请求,但没有服务端响应的示例:
TCP服务器:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* TCP 服务端
*/
public class TCPServer {
public ServerSocket serverSocket = null;
//对serversocket 进行初始化
//服务端的ServerSocket 需要指定端口号
public TCPServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//创建一个固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void start() throws IOException {
//1.等待建立连接
System.out.println("服务端已启动,等待客户端建立连接...");
//没有客户端请求的话, accept会一直被阻塞, 直到有客户端来请求建立连接
//clientSocket 就类似餐馆返回给用户的小票
while (true){
Socket clientSocket = serverSocket.accept();
//2.接收数据并处理数据
executorService.submit(()->{
process(clientSocket);//此时, 服务端返回Socket, 服务端和客户端已经建立连接
});
}
}
private void process(Socket clientSocket) {
System.out.printf("和客户端建立连接[%s:%d]\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream()){
//方便起见, 使用Scanner
Scanner scanner = new Scanner(inputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("和客户端断开连接[%s:%d]\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort());
break;
}
String request = scanner.nextLine();
System.out.printf("接收到客户端发送的数据[%s:%d]:%s\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort(),request);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
TCPServer server = new TCPServer(9090);
server.start();
}
}
TCP客户端:
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP 客户端
*/
public class TCPClient {
//建立连接
public Socket socket = null;
public TCPClient(String serverIp, int serverPort) throws IOException {
//如果服务端没有启动, 连接是建立不成功的
socket = new Socket(serverIp,serverPort);//和服务端建立连接
}
public void start(){
System.out.println("和服务端建立连接....");
try(OutputStream outputStream = socket.getOutputStream()) {
PrintWriter writer = new PrintWriter(outputStream);
//发送数据
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请输入发送的数据:");
String request = scanner.nextLine();//从控制台输入发送的数据
writer.println(request);
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPClient client = new TCPClient("127.0.0.1",9090);
client.start();
}
}
启动两个客户端,并发送不同的数据:
服务端接收到数据后:
以上客户端与服务端建立的为短连接,每次客户端发送了TCP报文,及服务端接收了TCP报文后,双方 都会关闭连接。
(5)示例二:请求响应(短连接)
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.HashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* TCP 服务端
*/
public class TCPDictServer {
public ServerSocket serverSocket = null;
public HashMap<String,String> dictMap = new HashMap<>();
//对serversocket 进行初始化
//服务端的ServerSocket 需要指定端口号
public TCPDictServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
dictMap.put("cat","猫");
dictMap.put("dog","狗");
dictMap.put("pig","猪");
dictMap.put("fox","狐狸");
dictMap.put("frog","青蛙");
}
//创建一个固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void start() throws IOException {
//1.等待建立连接
System.out.println("服务端已启动,等待客户端建立连接...");
//没有客户端请求的话, accept会一直被阻塞, 直到有客户端来请求建立连接
//clientSocket 就类似餐馆返回给用户的小票
while (true){
Socket clientSocket = serverSocket.accept();
//2.接收数据并处理数据
executorService.submit(()->{
process(clientSocket);//此时, 服务端返回Socket, 服务端和客户端已经建立连接
});
}
}
private void process(Socket clientSocket) {
System.out.printf("和客户端建立连接[%s:%d]\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()) {
//方便起见, 使用Scanner
Scanner scanner = new Scanner(inputStream);
while (true){
if (!scanner.hasNext()){
System.out.printf("和客户端断开连接[%s:%d]\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort());
break;
}
String request = scanner.nextLine();
//对接收的数据, 进行处理(翻译)
String response = processRequest(request);
//把响应内容, 返回给客户端
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("接收到客户端发送的数据[%s:%d]:%s,返回响应数据: %s\n",
clientSocket.getInetAddress().getHostName(),clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String processRequest(String request) {
return dictMap.getOrDefault(request,"无法翻译");
}
public static void main(String[] args) throws IOException {
TCPDictServer server = new TCPDictServer(9090);
server.start();
}
}
TCP客户端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP 客户端
*/
public class TCPDictClient {
//建立连接
public Socket socket = null;
public TCPDictClient(String serverIp, int serverPort) throws IOException {
//如果服务端没有启动, 连接是建立不成功的
socket = new Socket(serverIp,serverPort);//和服务端建立连接
}
public void start(){
System.out.println("和服务端建立连接....");
try(OutputStream outputStream = socket.getOutputStream()) {
try(InputStream inputStream = socket.getInputStream()){
PrintWriter writer = new PrintWriter(outputStream);
//发送数据
Scanner scanner = new Scanner(System.in);//此行代码, 是为了从控制台拿到数据
while (true){
System.out.println("请输入发送的数据:");
String request = scanner.nextLine();//从控制台输入发送的数据
writer.println(request);
writer.flush();
//接收响应
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.nextLine();
System.out.printf("发送数据:%s,接收到响应数据:%s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPDictClient client = new TCPDictClient("127.0.0.1",9090);
client.start();
}
}
启动服务端,进行翻译:
启动客户端:
TCP与UDP都是传输层的协议,其他层的协议都要和传输层的协议相匹配。