Socket 网络编程
网络基础
网络通信
两台设备之间通过网络实现数据传输
网络通信是指通过网络将数据从一台设备传输到另一台设备
java 在java.net 包下提供了一系列的类或接口,来完成网络编程
网络
IP
IPV6 使用128位表示:16个字节,十六进制表示
IP地址分类
域名
为了方便记忆,将ip地址映射成域名,解决记ip的困难
比如我们经常访问百度,使用的域名 www.baidu.com 相比 ip 180.101.50.188 方便好记
端口号
用来标识计算机上运行的网络程序,网络服务都会监听一个端口,我们通过ip + 端口来访问程序。
端口范围 0~65535 ,其中 0~1024 已经被占用,比如常见的 ssh:22 ftp:21 smtp:25 http: 80
常见的网络程序端口
tomcat: 8080
mysql: 3306
oracle: 1521
sqlserver: 1433
网络协议
协议是数据的组织形式。通过协议将数据准确无误的发送给接收方,协议是共同遵守的规范
OSI模型
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
TCP/IP 模型
物理层+数据链路层、网络层(IP)、传输层 (TCP)、应用层
TCP 和 UDP
TCP协议:传输控制协议
- 面向连接的: 使用TCP协议前,须先建立连接
- 可靠的:三次握手建立连接,可靠的传输
- 数据量大:连接中可进行 ,大数据量的传输
- 效率低:传送结束后需释放已建立的连接 (四次挥手)
UDP:用户数据报协议
- 无连接:将数据,源、目的封装成数据包,不需要建立连接
- 少量数据:每个数据包的大小限制64kB
- 不可靠:因为是无连接的,所以不可靠
- 速度快:发送完数据不需要释放资源(不是面向连接的),速度快
InetAddress
InetAddress
类是 Java 中用于表示 IP 地址的类,它提供了一种在网络编程中获取和操作 IP 地址信息的方式。InetAddress
类的主要作用是用于域名解析和 IP 地址相关的操作。
以下是 InetAddress
类的一些常用方法和作用:
getByName(String host)
: 这是InetAddress
类的静态方法之一,用于通过主机名获取InetAddress
对象。主机名可以是域名或 IP 地址。
InetAddress address = InetAddress.getByName("www.baidu.com");
getLocalHost()
: 也是InetAddress
类的静态方法,用于获取本地主机的InetAddress
对象。
InetAddress localHost = InetAddress.getLocalHost();
getHostAddress()
: 该方法返回 IP 地址的字符串表示形式。
String ipAddress = address.getHostAddress();
getHostName()
: 该方法返回主机的规范化名称。如果无法解析主机名,则返回 IP 地址的字符串形式。
String hostName = address.getHostName();
isReachable(int timeout)
: 该方法用于检查主机是否可达。它发送一个 ICMP 报文(ping)来测试主机的可达性,并在指定的超时时间内等待响应。如果主机可达,返回true
;否则返回false
。
boolean isReachable = address.isReachable(3000); // 3秒超时
getAllByName(String host)
: 该方法返回与指定主机名关联的所有 IP 地址。通常,一个主机名可能对应多个 IP 地址。
InetAddress[] addresses = InetAddress.getAllByName("www.example.com");
toString()
: 返回InetAddress
对象的字符串表示形式,通常是 IP 地址的字符串。
String addressStr = address.toString();
equals(Object obj)
** 和hashCode()
:*用于比较两个InetAddress
对象是否相等以及获取哈希码。
InetAddress
类主要用于网络编程中,例如在网络通信、域名解析、服务发现等方面。通过它,你可以获取主机的 IP 地址、判断主机的可达性、进行网络连接等操作。
Socket
socket 是一种应用于网络编程的接口,它将负责的底层通信网络封装使开发者更容易的编写网络应用,完成在不同计算机上的应用程序可以通过网络进行数据交换和通信
以下是关于Socket的一些重要概念和解释:
- 套接字(Socket): Socket是一种用于网络通信的编程接口,它位于应用层和传输层之间,允许应用程序通过网络发送和接收数据。Socket提供了一组API,用于建立、连接、发送和接收数据。
- 网络套接字(Socket): 网络套接字是通过IP地址和端口号来唯一标识的,它用于在网络上定位和识别特定的应用程序或服务。一个套接字通常由两个端点组成,一个是服务器端套接字,另一个是客户端套接字。
- 服务器套接字(Server Socket): 服务器套接字是用于等待客户端连接请求的套接字。一旦客户端请求连接,服务器套接字会接受连接并创建一个新的套接字,用于与客户端进行通信。
- 客户端套接字(Client Socket): 客户端套接字是用于与服务器建立连接并进行通信的套接字。客户端套接字通过连接到服务器套接字来建立通信通道。
- 协议: Socket通常使用特定的网络协议来进行通信,例如TCP(传输控制协议)或UDP(用户数据报协议)。这些协议定义了数据在网络上的传输方式和规则。
- 阻塞和非阻塞: Socket可以以阻塞或非阻塞方式运行。在阻塞模式下,Socket调用会一直等待,直到操作完成。在非阻塞模式下,Socket调用会立即返回,不管操作是否完成,允许程序继续执行其他任务。
- 通信模型: Socket可以用于实现不同的通信模型,包括客户端-服务器模型和对等通信模型。在客户端-服务器模型中,一个服务器等待客户端连接并提供服务。在对等通信模型中,两个计算机之间可以直接通信,不需要中央服务器。
netstat 指令的使用
- 使用 neatest -an | 查看当前主机网络情况,包括端口监听情况和网络连接情况
- netstat -an | more 可以分页显示
说明:
LISTEN 表示端口在监听 (windosw 上是 LISTENING)
如果有一个外部程序(客户端)连接到该端口,Foreign Address 就会显示一条连接信息
ctrl + c 退出
客户端也是有端口的
客户端连接到服务端后,其实客户端也是有端口的,这个端口是TCP/IP 随机分配的
可以在客户端上传一个大文件,来查看随机分配给客户端的端口
基于TCP的网络编程
案例1 client -> server 简单通信
需求:
使用字节流的方式,客户端向 本地端口9527 的服务服务端发送 hello server , 服务端监听 端口9527 接收客户端信息之后结束通信
代码实现:
SocketServer01.java
/**
* @Description: 服务端
* @Author : wy
* @Date : Created 2023/9/10 5:25 PM
*/
public class SocketServer01 {
public static void main(String[] args) throws IOException {
//1. 创建serverSocket 对象,监听 9527 端口
// 注意: 如果监听的端口被占用了,服务端将会启动失败
ServerSocket serverSocket = new ServerSocket(9527);
System.out.println("服务端已启动,监听 9527 端口");
//2. 获取socket 对象,如果未收到客户端的消息,将会阻塞程序,接收到后继续执行下面逻辑
Socket socket = serverSocket.accept();
//3. 通过socket 获取该连接的 输入流对象
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen = 0;
//4. 读取数据,输出到控制台
while ( (readLen = inputStream.read(buf)) != -1){
//根据读到的实际长度输出
System.out.println(new String(buf,0,readLen));
}
//5. 释放资源
inputStream.close();
socket.close();
serverSocket.close();
System.out.println("服务端已关闭...");
}
}
SocketClient01.java
/**
* @Description: 客户端
* @Author : wy
* @Date : Created 2023/9/10 5:39 PM
*/
public class SocketClient01 {
public static void main(String[] args) throws IOException {
//1. 通过 本地ip + 9527 端口 建立 socket 连接
Socket socket = new Socket(InetAddress.getLocalHost(),9527);
//2. 建立连接后根据得倒的 socket 对象获取一个输出流对象
OutputStream outputStream = socket.getOutputStream();
//3. 通过输出流write ,发送数据
outputStream.write("hello server".getBytes());
//4. 关闭输出流、socket 释放连接资源
outputStream.close();
socket.close();
System.out.println("客户端已退出...");
}
}
案例2 client -> server -> client
需求:
在案例1的基础上,使用字节流的方式 ,客户端向 本地端口9527 的服务服务端发送 hello server , 服务端监听 端口9527 接收客户端信息之后给 发送消息的客户端返回 hello client ,客户端接受到服务端返回的消息后结束通信。
代码实现:
SocketServer02.java
/**
* @Description: 服务端
* @Author : wy
* @Date : Created 2023/9/10 5:25 PM
*/
public class SocketServer02 {
public static void main(String[] args) throws IOException {
//1. 创建serverSocket 对象,监听 9527 端口
// 注意: 如果监听的端口被占用了,服务端将会启动失败
ServerSocket serverSocket = new ServerSocket(9527);
System.out.println("服务端已启动,监听 9527 端口");
//2. 获取socket 对象,如果未收到客户端的消息,将会阻塞程序,接收到后继续执行下面逻辑
Socket socket = serverSocket.accept();
//3. 通过socket 获取该连接的 输入流对象
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen = 0;
//4. 读取数据,输出到控制台
while ( (readLen = inputStream.read(buf)) != -1){
//根据读到的实际长度输出
System.out.println(new String(buf,0,readLen));
}
//5. 向服务端返回消息
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello client".getBytes());
//6. 释放资源
inputStream.close();
socket.close();
serverSocket.close();
System.out.println("服务端已关闭...");
}
}
SocketClient02.java
/**
* @Description: 客户端
* @Author : wy
* @Date : Created 2023/9/10 5:39 PM
*/
public class SocketClient02 {
public static void main(String[] args) throws IOException {
//1. 通过 本地ip + 9527 端口 建立 socket 连接
Socket socket = new Socket(InetAddress.getLocalHost(),9527);
//2. 建立连接后根据得倒的 socket 对象获取一个输出流对象
OutputStream outputStream = socket.getOutputStream();
//3. 通过输出流write ,发送数据
outputStream.write("hello server".getBytes());
//4. 添加 结束标志
socket.shutdownOutput();
//5.接收服务端收到消息后的返回信息
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen = 0;
while ((readLen = inputStream.read(buf)) != -1){
System.out.println(new String(buf,0,readLen));
}
//5. 关闭输出流、socket 释放连接资源
outputStream.close();
inputStream.close();
socket.close();
System.out.println("客户端已退出...");
}
}
案例3 使用字符流的方式
在案例2的基础上改造下使用字符输入、输出流来发送接收数据
具体代码:
SocketServer03.java
/**
* @Description: 服务端
* @Author : wy
* @Date : Created 2023/9/10 5:25 PM
*/
public class SocketServer03 {
public static void main(String[] args) throws IOException {
//1. 创建serverSocket 对象,监听 9527 端口
// 注意: 如果监听的端口被占用了,服务端将会启动失败
ServerSocket serverSocket = new ServerSocket(9527);
System.out.println("服务端已启动,监听 9527 端口");
//2. 获取socket 对象,如果未收到客户端的消息,将会阻塞程序,接收到后继续执行下面逻辑
Socket socket = serverSocket.accept();
//3. 通过socket 获取该连接的 输入流对象
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
if ((line = reader.readLine()) != null) {
System.out.println(line);
}
//5. 向服务端返回消息
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("hello client");
writer.flush();
socket.shutdownOutput();
//6. 释放资源
writer.close();
reader.close();
socket.close();
serverSocket.close();
System.out.println("服务端已关闭...");
}
}
SocketClient03.java
/**
* @Description: 客户端
* @Author : wy
* @Date : Created 2023/9/10 5:39 PM
*/
public class SocketClient03 {
public static void main(String[] args) throws IOException {
//1. 通过 本地ip + 9527 端口 建立 socket 连接
Socket socket = new Socket(InetAddress.getLocalHost(),9527);
//2. 使用字符输出流的write ,发送数据
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("hello server");
writer.flush();
socket.shutdownOutput();
//4.接收服务端收到消息后的返回信息
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null){
System.out.println(line);
}
//5. 关闭输出流、socket 释放连接资源
writer.close();
reader.close();
socket.close();
System.out.println("客户端已退出...");
}
}
注意⚠️: 使用字符输出流,时要切记调用 fulsh( ) 将数据刷新,并设置结束标志。这样接收方才能正确判断数据发送结束,不在接收数据。不然服务端和客户端会阻塞。
案例4 网络上传文件
需求:
- 编写一个服务端,和客户端
- 服务端监听 9527 端口
- 客户端连接服务端,从磁盘获取一张图片发送给服务端
- 服务端接收到 客户端发送的图片后,保存到 src 下,发送 “收到图片” 再退出
- 客户端接收到服务端发送的 “收到图片” 再退出
工具类:
StreamUtil.java
streamToByteArray(InputStream is) 将输入流转换为 byte[], 既可以把文件内容读入到 byte[]
streamToString(InputStream is) 将输入InputStream 中的数据 转换成 String
/**
* @Description: 字节流转换工具类
* @Author : wy
* @Date : Created 2023/9/11 4:43 PM
*/
public class StreamUtil {
/**
* 将输入流转换为 byte[], 既可以把文件内容读入到 byte[]
* @param is 字节输入流
* @return byte[]
* @throws IOException
*/
public static byte[] streamToByteArray(InputStream is) throws IOException {
//创建输出流对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//字节数组
byte[] b = new byte[1024];
int len;
while ((len = is.read(b)) != -1) {
//循环读取,把读取的数据写到 输出流中
bos.write(b,0,len);
}
//将输出流转为 字节数组
byte[] array = bos.toByteArray();
bos.close();
return array;
}
/**
* InputStream 转换成 String
* @param is 字节输入流
* @return
* @throws IOException
*/
public static String streamToString(InputStream is) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder stringBuilder = new StringBuilder();
String line;
while((line = reader.readLine()) != null) {
stringBuilder.append(line).append("\r\n");
}
return stringBuilder.toString();
}
}
代码实现:
FileUploadService.java
/**
* @Description: 文件上传服务端
* @Author : wy
* @Date : Created 2023/9/11 6:53 PM
*/
public class FileUploadService {
public static void main(String[] args) throws IOException {
//监听本地 9527 端口
ServerSocket serverSocket = new ServerSocket(9527);
System.out.println("服务端在 9527 端口启动");
Socket socket = serverSocket.accept();
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
byte[] bytes = StreamUtil.streamToByteArray(bis);
//将到得的字节数组写入到 执行文件
String destPath = "src/motorcycle2.jpg";
BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get(destPath)));
bos.write(bytes);
bos.close();
//向 客户端回复
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("收到图片");
writer.flush();
socket.shutdownOutput();
//关闭其他资源
writer.close();
bis.close();
socket.close();
serverSocket.close();
System.out.println("服务器结束...");
}
}
FileUploadClient.java
/**
* @Description: 文件上传客户端
* @Author : wy
* @Date : Created 2023/9/11 6:53 PM
*/
public class FileUploadClient {
public static void main(String[] args) throws IOException {
//创建连接
Socket socket = new Socket(InetAddress.getLocalHost(),9527);
//从本地磁盘获取上传文案
String filePath = "/Users/yang/Desktop/imgs/motorcycle.jpg";
BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
//使用 工具类 将字节流转为 byte[]
byte[] bytes = StreamUtil.streamToByteArray(bis);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//将文件内容写入 输出流中
bos.write(bytes);
bis.close();
//设置结束标志
socket.shutdownOutput();
//获取 服务端 收到图片后的返回信息
String res = StreamUtil.streamToString(socket.getInputStream());
System.out.println(res);
//关闭资源
bos.close();
socket.close();
System.out.println("客户端结束...");
}
}
案例5 网络下载文件
需求:
客户端向服务端发送要下载的文件名,服务端接收到后根据文件名返回给客户端文件内容,如果根据文件名没有找到返回一个默认的文件
代码实现:
FileDownloadServer.java
/**
* @Description: 文件下载服务端
* @Author : wy
* @Date : Created 2023/9/14 7:10 PM
*/
public class FileDownloadServer {
public static void main(String[] args) throws IOException {
//监听 9527 端口
ServerSocket serverSocket = new ServerSocket(9527);
System.out.println("服务端启动,监听 9527 端口,等待客户端连接...");
//创建 socket 等待客户端连接
Socket socket = serverSocket.accept();
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
int len = 0;
byte[] buf = new byte[1024];
StringBuilder downloadFileName = new StringBuilder();
while ((len = bis.read(buf)) != -1) {
downloadFileName.append(new String(buf, 0, len));
}
System.out.println("收到来自客户端的下载请求,下载 " + downloadFileName);
String filePath = "";
if ("motorcycle2".contentEquals(downloadFileName)){
filePath = "src/motorcycle2.jpg";
}else {
filePath = "src/dog.jpeg";
}
//读取文件,得到字节数组
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
byte[] bytes = StreamUtil.streamToByteArray(bufferedInputStream);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
socket.shutdownOutput();
outputStream.close();
//关闭流
bufferedInputStream.close();
bis.close();
socket.close();
serverSocket.close();
System.out.println("服务端关闭");
}
}
FileDownloadClient.java
/**
* @Description: 文件下载客户端
* @Author : wy
* @Date : Created 2023/9/14 7:09 PM
*/
public class FileDownloadClient {
public static void main(String[] args) throws IOException {
//连接服务端
Socket socket = new Socket(InetAddress.getLocalHost(),9527);
System.out.println("客户端已启动...");
//读取用户输入的文件名
Scanner scanner = new Scanner(System.in);
System.out.println("请输入下载文件名:");
String fileName = scanner.next();
//获取输出流
OutputStream outputStream = socket.getOutputStream();
outputStream.write(fileName.getBytes());
//设置输入结束标志
socket.shutdownOutput();
//获取输入流,得倒文件数据的字节数组
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
byte[] bytes = StreamUtil.streamToByteArray(bis);
//将文件保存到指定路径
String destPath = "/Users/yang/" + fileName + ".jpg";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath));
bos.write(bytes);
//关闭资源
bos.close();
bis.close();
outputStream.close();
socket.close();
System.out.println("文件下载完成...");
}
}
基于UDP的网络编程
通过 udp 方式实现 socket 网络编程
基本介绍
- DatagramSocket 和 DatagramPacket 类实现了基于UDP协议的网络编程
- UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP 数据报一定能够安全的送到目的地,也不确定什么时候可以抵达
- DatagramPacket 对象封装了UDP 数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号
- UDP协议中每个数据报都给出了完整的地址信息,所以无须建立发送方和接收放的连接
UDP编程的基本流程
- 核心的两个类 DatagramSocket 和 DatagramPacket
- 建立发送端、接收端
- 建立数据包
- 调用DatagramSocket 的发送、接收方法
- 关闭 DatagramSocket
简单案例
需求:
接收方 A ,收到发送 B 发来的消息后 给 B 回复消息后退出,B 接收到 A 的回复消息后也退出
代码实现:
UDPReceiverA.java
/**
* @Description: udp接收端
* @Author : wy
* @Date : Created 2023/9/12 11:38 PM
*/
public class UDPReceiverA {
public static void main(String[] args) throws IOException {
//创建一个 DatagramSocket 准备在 9527 端口接收数据
DatagramSocket socket = new DatagramSocket(9527);
//构建一个DatagramPacket 来接收数据
byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length);
// 调用接收方法将数据 填充到 packet
// 当有数据发送到 监听端口 9527 上时就会接收数据,如果没有则阻塞等待
System.out.println("准备接收发送方的消息 ..");
socket.receive(datagramPacket);
//把packet 拆包,取出数据
int length = datagramPacket.getLength(); //接收到的数据长度
byte[] data = datagramPacket.getData(); //接收到的数据
String msg = new String(data,0,length);
System.out.println(msg);
//回复信息给 B 端
byte[] sendMsg = "月亮不睡我不睡,我是秃头小宝贝~".getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendMsg,sendMsg.length, InetAddress.getByName("127.0.0.1"),9998);
socket.send(sendPacket);
socket.close();
System.out.println("A 端退出");
}
}
UDPSenderB.java
public class UDPSenderB {
public static void main(String[] args) throws IOException {
// 创建 DatagramSocket 对象,准备在 9998 端口接收数据
DatagramSocket socket = new DatagramSocket(9998);
byte[] sendMsg = "晚上早点睡!".getBytes();
//packet 封装消息,具体给出消息 、消息长度、接收端的 ip 、接收端的 port
DatagramPacket packet = new DatagramPacket(sendMsg,sendMsg.length, InetAddress.getByName("127.0.0.1"),9527);
//发送消息
socket.send(packet);
//接收 消息
byte[] buf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(buf,buf.length);
socket.receive(receivePacket);
//从receivePacket中取出消息
int len = receivePacket.getLength();
byte[] res = receivePacket.getData();
String receiveMsg = new String(res,0,len);
System.out.println(receiveMsg);
socket.close();
System.out.println("B 端退出");
}
}