网络编程 (套接字)
网络编程概述:网络编程即使用套接字来达到进程间通信
基本概念
网络的七层模型 (OSI):物理层、数据链路层、网络层、传输层 (UDP、TCP)、会话层、表示层、应用层 (http\https\ftp)
IP地址:唯一标识网络中的通信实体、
- IPV4:32位二进制组成;分成4组,每一组的范围0-255之间,每组之间通过.来分隔;例如:192.168.12.22
- IPV6:128位二进制组成;分成了8组,每一组16位,十六进制;每一组由4个十六进制数字组成;每组之间通过:来分隔;
- IP不能重复
- IP本身是动态分配的,静态IP可以实现但是收费高昂;
- 广播IP地址:255.255.255.255
端口号:
-
数据发送还是数据接收都需要通过端口号
-
给端口进行了一个编号:0-65535
-
0-1024这些端口已经被系统内部或者通用的协议占用了
-
80:缺省端口
域名:将IP地址通过一个字符串来代替
DNS: 域名解析服务器
- 提供根据域名查询IP地址的服务
- 本机hosts文件位置:C盘 —> Windows —> System32 —> drivers —> etc —> hosts文件
- 首先检查本机的hosts文件中有没有域名的配置,如果有直接连接对应的IP地址,否则在DNS上查找对应的IP地址
代表IP地址和端口的类 — SocketAddress
-
SocketAddress
为抽象类 -
实现类:
InetSocketAddress
— 实现 IP 套接字地址(IP 地址 + 端口号) -
构造方法:
InetSocketAddress(String hostname, int port):
根据主机名和端口号创建套接字地址。
-
方法:
String getHostName()
:获取 hostname。int getPort()
:获取端口号。
public class SocketAddressDemo { public static void main(String[] args) { // 创建InetSocketAddress对象 表示IP和端口 // 第一个参数表示IP地址或者主机名,第二个地址表示端口号 // 127.0.0.1 localhost 永远指向本机 InetSocketAddress isa = new InetSocketAddress("127.0.0.1",3344); // 获取主机名 // 如果是本机返回主机名;如果是其他机器,则返回传入的ip地址 System.out.println(isa.getHostName()); // 获取端口号 System.out.println(isa.getPort()); } }
TCP / UDP
UDP
-
用户数据报协议(User Datagram Protocol)
-
数据报(Datagram):网络传输的基本单位
-
UDP特点:
-
不建立连接
-
不可靠
-
数据传输效率高
-
-
应用场景:对数据可靠性要求不太高,但是对数据传输效率要求比较高;例如:语音通话、视频通话、视频直播
使用UDP进行网络数据传输
- 底层基于流实现,使用JDK提供的
DatagramSocket
类
DatagramSocket
-
此类表示用来发送和接收数据报包的套接字;
-
构造方法:
DatagramSocket()
:构造数据报套接字并将其绑定到本地主机上任何可用的端口DatagramSocket(int port)
:创建数据报套接字并将其绑定到本地主机上的指定端口。
-
方法:
void send(DatagramPacket p)
:从此套接字发送数据报包。void receive(DatagramPacket p)
:从此套接字接收数据报包。void close()
:用于关闭此数据报套接字
DatagramPacket:
-
此类表示数据报包
-
构造方法:
- 发送端构造方法:
DatagramPacket(byte[] buf, int length, SocketAddress address)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。 - 接收端构造方法:
DatagramPacket(byte[] buf, int length)
构造DatagramPacket
,用来接收长度为 length 的数据包。
- 发送端构造方法:
-
方法:
byte[] getData()
:获取数据内容。int getLength()
:返回将要发送或接收到的数据的长度SocketAddress getSocketAddress()
:获取要将此包发送的或发出此包的远程主机的SocketAddress
发送端和接收端的流程
发送端:
- 指定:IP + 端口号
- 创建发送端的对象:
DatagramSocket
- 准备数据报包:
DatagramPacket
- 调用方法发送数据
- 关流
public class UDPSenderDemo {
public static void main(String[] args) throws IOException {
// 发送端
// 1. 创建发送端的对象:DatagramSocket
DatagramSocket ds = new DatagramSocket();
// 2. 准备数据报包:DatagramPacket
// 2.1 第一个参数: byte[] buf 表示要发送的数据
String str = "hello world";
byte[] bytes = str.getBytes();
// 2.2 第二个参数: int length 发送数据的长度
// 2.3 第三个参数: InetSocketAddress
InetSocketAddress isa = new InetSocketAddress("127.0.0.1",3456);
DatagramPacket dp = new DatagramPacket(bytes,str.length(),isa);
// 3. 调用方法发送数据
ds.send(dp);
// 4. 关流
ds.close();
}
}
接收端:
- 指定:监听端口号
- 创建接收端的对象
DatagramSocket
,并指定端口号 - 准备接受数据的数据报包
DatagramPacket
- 调用接受数据的方法
- 关流
- 解析数据报包,并打印内容
public class UDPReceiveDemo {
public static void main(String[] args) throws IOException {
// 接收端
// 1. 创建接收端的对象DatagramSocket,并指定端口号
DatagramSocket ds = new DatagramSocket(3456);
// 2. 准备接受数据的数据报包DatagramPacket
// 2.1 第一个参数 byte[] bytes 用于接收数据
// 2.2 第二个参数 指定数组可以使用的长度,byte数组长度
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
// 3. 调用接受数据的方法
// 等待发送端发送数据
ds.receive(dp);
// 4. 关流
ds.close();
// 5. 解析数据报包,并打印内容
byte[] data = dp.getData();
System.out.println(new String(data,0,dp.getLength()));
}
}
注意:先运行接收端在receive()
处阻塞,等待接收数据,再运行发送端发送数据
案例演示:使用UDP模拟单人聊天
public class ChatUDPDemo {
public static void main(String[] args) {
// 创建对象
new Thread(new ChatReceiver()).start();
new Thread(new ChatSender()).start();
}
}
// 定义发送端的类实现Runnable接口
class ChatSender implements Runnable {
@Override
public void run() {
// 定义UDP发送数据类
// DatagramSocket ds = null;
try {
DatagramSocket ds = new DatagramSocket();
// 准备Scanner对象用于获取用户输入的数据
Scanner sc = new Scanner(System.in);
while (true) { // 不断发送数据
// 获取需要发送的字符串
// 阻塞
String msg = sc.next();
// 创建数据报包
DatagramPacket dp = new DatagramPacket(msg.getBytes(), msg.length(),
new InetSocketAddress("127.0.0.1", 3345));
// 发送数据
ds.send(dp);
if ("#".equals(msg)) { // 结束发送条件
break;
}
}
// 关流
ds.close();
sc.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 定义接收端的类,实现Runnable接口
class ChatReceiver implements Runnable {
@Override
public void run() {
// 定义UDP接收数据类
// DatagramSocket ds = null;
try {
DatagramSocket ds = new DatagramSocket(3345);
// 端口号必须一致
while (true) {
// 准备接收数据的数据报包
DatagramPacket dp = new DatagramPacket(new byte[200], 200);
// 接收数据
// 阻塞
ds.receive(dp);
// 解析数据
String msg = new String(dp.getData(), 0, dp.getLength());
if ("#".equals(msg)) {
System.out.println(dp.getAddress().getHostName() + "已下线");
break;
}
System.out.println(dp.getAddress().getHostName() + ":" + msg);
}
// 关流
ds.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
TCP
-
传输控制协议 (Transmission Control Protocol)
-
TCP特点:
-
建立连接(三次握手)
-
可靠的
-
数据传输效率低
-
不限制传输数据量
-
-
应用场景:数据可靠性要求非常高,对数据传输效率没有特别高的要求;例如:文件传输 QQ文字消息
使用TCP进行网络数据传输
- TCP底层基于流的
客户端:Socket类
- 此类实现客户端套接字
- 构造方法
Socket()
:通过系统默认类型的 SocketImpl 创建未连接套接字Socket(String host, int port)
:创建一个流套接字并将其连接到指定主机上的指定端口号。
- 方法:
void connect(SocketAddress endpoint)
:将此套接字连接到服务器InputStream getInputStream()
: 返回此套接字的输入流OutputStream getOutputStream()
: 返回此套接字的输出流void shutdownInput()
: 读取完成void shutdownOutput()
: 输出完成。
服务器端:ServerSocket
- 此类实现服务器套接字
- 构造方法
ServerSocket(int port)
:创建绑定到特定端口的服务器套接字。
- 方法
Socket accept()
: 接受客户端的连接请求。 返回一个代表当前正在连接的客户端对象void close()
:关流
TCP实现网络数据传输
- 客户端
- 创建客户端对象 —
Socket
- 发起连接服务器端的请求 —
connect()
- 获取输出流 —
getOutputStream()
- 写出数据 —
write()
- 通知服务器端数据写出完成 —
shutdownOutput()
- 关流 —
close()
- 创建客户端对象 —
public class TCPClientDemo {
public static void main(String[] args) throws IOException {
// 1. 创建客户端对象
Socket socket = new Socket();
// 2. 发起连接服务器端的请求
// 需要一个参数指定服务器端的ip地址和端口号
socket.connect(new InetSocketAddress("127.0.0.1",8888));
// 3. 获取输出流
OutputStream out = socket.getOutputStream();
// 4. 写出数据
out.write("hello".getBytes());
// 5. 通知服务器已经写出完成
socket.shutdownOutput();
// 6. 关闭客户端
socket.close();
}
}
- 服务器端:
- 创建服务器端对象,监听端口 —
ServerSocket
- 接受客户端的请求 —
accept()
- 获取一个输入流 —
getInputStream()
- 读取数据 —
read()
- 通知客户端读取完成 —
shutdownInput()
- 关流 —
close()
- 创建服务器端对象,监听端口 —
public class TCPServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建服务器端对象 ServerSocket,绑定端口号
ServerSocket ss = new ServerSocket(8888);
// 2. 接受客户端请求
// accept() --- 阻塞
Socket socket = ss.accept();
// 3. 获取一个输入流
InputStream in = socket.getInputStream();
// 4. 读取数据
byte[] bytes = new byte[1024];
int len;
while ((len = in.read(bytes)) != -1) {
// 打印数据
System.out.println(new String(bytes, 0, len));
}
// 5. 通知客户端,数据读取完成
socket.shutdownInput();
// 6. 关闭流
ss.close();
}
}
案例演示:使用TCP通信模拟文件传输流程
import java.io.*;
import java.net.*;
public class FileUploadDemo {
public static void main(String[] args) {
Server server = new Server();
new Thread(server).start();
Client client = new Client();
new Thread(client).start();
}
}
class Client implements Runnable{
@Override
public void run() {
// 1. 创建客户端对象
Socket socket = new Socket();
try {
// 2. 连接服务器端
socket.connect(new InetSocketAddress("127.0.0.1",8888));
// 3. 输入流,读取本地文件
File file = new File("D:\\Javase\\Day21\\src\\test.txt");
InputStream in = new FileInputStream(file);
// 获取文件名
String fileName = file.getName();
// 4. 获取一个输出流
OutputStream out = socket.getOutputStream();
// 5. 读取本地文件,发送给服务器端
// 5.1 发送文件名(长度 + 名称)
out.write(fileName.length());
out.write(fileName.getBytes());
// 5.2 发送内容
byte[] bytes = new byte[1024];
int len;
while ((len = in.read(bytes)) !=-1){
out.write(bytes,0,len);
}
// 6. 通知服务器端文件发送完成
socket.shutdownOutput();
// 新增功能 获取服务器端的回复数据并打印
InputStream in1 = socket.getInputStream();
byte[] bytes1 = new byte[1024];
int len1;
while ((len1 = in1.read(bytes1)) !=-1){
System.out.println(new String(bytes1,0,len1));
}
// 通知服务器端数据读取完成
socket.shutdownInput();
// 7. 关闭所有流
socket.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Server implements Runnable{
@Override
public void run() {
// 1. 创建服务器对象
try {
ServerSocket ss = new ServerSocket(8888);
// 2. 接受客户端连接请求
Socket socket = ss.accept();
// 3. 获取一个输入流
InputStream in = socket.getInputStream();
// 4. 准备一个输出流 将数据写出到e
// 4.1 读取文件名长度
int fileNameLen = in.read();
// 4.2 读取文件名称
byte[] b = new byte[fileNameLen];
in.read(b);
String fileName = new String(b);
OutputStream out = new FileOutputStream("D:\\Javase\\Day21\\src\\testFile\\" + fileName);
// 5. 一边通过输入流读取客户端发送的数据,一边写出
// 读取内容
int len;
byte[] bytes = new byte[1024];
while ((len = in.read(bytes)) != -1){
// 当前已经接收了客户端发送的一部分数据
// 将这部分数据写出
out.write(bytes,0,len);
}
// 6. 通知客户端文件接收完成
socket.shutdownInput();
// 新增功能:当文件接受完成,给客户端一个回复
OutputStream out1 = socket.getOutputStream();
out1.write("文件传输完成".getBytes());
// 通知客户端文件写出完成
socket.shutdownOutput();
// 7. 关闭所有流
socket.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}