网络编程详解
网络通信的要素
通信双方的地址
- IP地址
- 端口号
通信协议
TCP/IP 参考模型
应用层
HTTP、FTP、SMTP
传输层
TCP、UDP
网络层
IP
数据链路层
Java 中的网络支持:
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
- URL:统一资源定位符;
- Sockets:使用 TCP 协议实现网络通信;
- Datagram:使用 UDP 协议实现网络通信。
IP
IP地址的类:InetAddress
IP地址能唯一定位一台网络上的计算机
127.0.0.1:本机 localhost
IP地址的分类
IPV4/IPV6
公网/局域网
192.168.xx.xx(专门给组织内部使用)
域名
InetAddress没有公有的构造函数,只能通过静态方法来创建实例:
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
//测试IP
public class TestIP {
public static void main(String[] args) {
try {
//查询本机地址
InetAddress inetAddress1 = InetAddress.getByName("localhost");
System.out.println(inetAddress1);
InetAddress inetAddress2 = InetAddress.getLocalHost();
System.out.println(inetAddress2);
//查询网站ip地址
InetAddress inetAddress3 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress3);
//常用方法
System.out.println(inetAddress3.getCanonicalHostName()); //规范的主机名
System.out.println(inetAddress3.getHostAddress()); // IP
System.out.println(inetAddress3.getHostName()); // 域名或自己的计算机名
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
端口
端口表示计算机上一个程序的对外的接口。
不同的进程有不同的端口号,用来区分软件
规定端口号范围:0-65535
相同协议下,端口号不能冲突(不同协议,可以使用相同端口号)
端口分类:
公有端口:0-1023
一般会被内置的进程、服务器使用,尽量不要使用
- HTTP端口: 80
- HTTPS端口: 443
- FTP端口: 21
- SSH: 22
- Telent: 23
程序注册端口:1024-49151
分配给用户或程序
- Tomcat: 8080
- MySQL: 3306
- Oracle: 1521
- SQL Server:1433
动态、私有端口:49152-65535
netstat -ano #查看端口命令 netstat -ano|findstr "5900" #查看指定的端口 tasklist|findstr "1584" #查看指定端口的进程
public class TestSocketAddress {
public static void main(String[] args) {
InetSocketAddress socketAddress = new InetSocketAddress("localhost", 8080);
System.out.println(socketAddress);
System.out.println(socketAddress.getAddress());
System.out.println(socketAddress.getHostName());
System.out.println(socketAddress.getPort()); //获取端口
}
}
URL
URL(统一资源定位符)用于定位网络资源,可以直接从URL中读取字节流数据。
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu.com");
/* 字节流 */
InputStream is = url.openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
下载网络资源
URL url = new URL("https://pic3.zhimg.com/v2-77fd9b17781556ee25be582af6a9cd4c_r.jpg?source=1940ef5c"); // 资源地址(图片)
// 连接到该资源
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream is = urlConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is); // 图片、音频、视频等类型使用缓冲字节流,含中文的文档类资源使用缓冲字符流
// 判断文件夹是否存在
File file = new File("C:\\Users\\Hunter\\Downloads");
if (!file.exists()) {
file.mkdirs();
}
FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[4 * 1024]; // 存储每次读取的数据
int len = 0;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
bis.close();
urlConnection.disconnect();
通信协议
TCP/IP协议簇:
TCP(Transmission Control Protocol) 传输控制协议
UDP(User Datagram Protocol) 用户数据报协议
IP(Internet Protocol) 网络互连协议
TCP 和 UDP 的对比
TCP(类比:打电话)
需要连接,稳定
三次握手,四次挥手
// 至少需要三次,保证稳定连接 C:我喜欢你 S:我也喜欢你 C:我们在一起吧 // 四次挥手 C:我要走了 S:我知道你要走了 S:你已经走了吗? C:我已经走了
有明确的客户端和服务端角色
传输完成后释放连接,效率低
UDP(类比:发短信)
- 不需要连接,不稳定
TCP
Socket:套接字,指两台设备之间通讯的端点,是包含了IP地址和端口号的网络单位。
java.net.ServerSocket
服务器端类,相当于开启一个服务,并等待客户端的连接
java.net.Socket
客户端类,向服务器发出连接请求,服务端响应请求后,两者建立连接,开始通信(三次握手)
服务端
java.net.ServerSocket
构造方法
ServerSocket(int port)
:创建绑定到指定端口的服务端套接字
成员方法
Socket accept()
:侦听并接收客户端的连接
实现步骤
- 创建服务器端对象ServerSocket,指定端口
- 等待用户的连接,通过accept方法获取请求的客户端对象
- 使用获取到的客户端对象的getInputStream方法获取网络字节输入流InputStream对象
- 使用网络字节输入流对象的read方法读取客户端发来的数据
- 使用获取到的客户端对象的getOutputStream获取网络字节输出流OutputStream对象
- 使用网络字节输出流对象的write方法给客户端发送数据(传输结束标记)
- 释放资源
服务器没有IO流,而是通过获取请求的客户端对象Socket,使用Socket提供的IO流和客户端进行交互:
- 服务器使用客户端的字节输入流InputStream读取客户端发送的数据
- 服务器使用客户端的字节输出流OutputStream向客户端写回数据
ServerSocket serverSocket = new ServerSocket(2000);
while (true) { // 服务端的持久运行
// 2. 获取请求的客户端对象
Socket socket = serverSocket.accept();
// 3. 读取客户端消息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 向客户端发送数据
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write("你在教我做事?");
bw.newLine();
bw.flush(); // 重要
// 网络流的传输不同于本地,不会传输结束的标记
socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
br.close();
bw.close();
socket.close();
}
客户端
java.net.Socket
构造方法
Socket(String host, int port)
:创建一个套接字,并连接到指定主机上指定的端口号。
- String host:服务器主机的名称/服务器的IP地址
成员方法
OutputStream getOutputStream()
:返回套接字的输出流InputStream getInputStream()
:返回套接字的输入流void close()
:关闭套接字
实现步骤
- 创建一个客户端对象Socket
- 使用getOutputStream方法获取网络字节输出流OutputStream对象
- 使用网络字节输出流对象的write方法给服务器发送数据(传输结束标记)
- 使用getInputStream获取网络字节输入流InputStream对象
- 使用网络字节输入流对象的read方法读取服务器传回的数据
- 释放资源
Socket socket = new Socket("127.0.0.1", 2000);
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write("教练,我想打篮球");
bw.newLine();
bw.flush(); // 重要
// 网络流的传输不同于本地,不会传输结束的标记
socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
// 3. 读取服务端消息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
bw.close();
br.close();
socket.close();
文件上传
注意事项
- 为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如
WEB-INF
目录下。 - 为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名(时间戳 + uuid + md5加密)。
- 限制上传文件的最大值。
- 限制上传文件的类型,判断后缀名是否合法。
服务端
- 创建服务器ServerSocket对象
- 使用ServerSocket对象中的accept方法,获取到请求的客户端Socket对象
- 使用Socket对象的getInputStream方法,获取到网络字节输入流
- 判断指定存储的文件夹是否存在,不存在则创建
- 创建一个本地字节输出流FileOutputStream对象
- 读取客户端上传的文件,写入至服务器中指定的文件夹下
- 获取网络字节输出流对象,返回上传成功的消息(传输结束标记)
- 释放资源
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 判断文件夹是否存在
File file = new File("C:\\Users\\Hunter\\Downloads");
if (!file.exists()) {
file.mkdirs();
}
// 使用【域名 + 系统时间 + 随机数】用于命名
FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMills() + new Random().nextInt(999999) + ".md");
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
// 将本地上传的文件内容输出至指定文件
String line;
while ((line = br.readLine()) != null) { // 客户端socket.shutdownOutput()的操作,会传来文件的结束标记
bw.write(line);
bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
}
bw.close();
// 返回上传成功的消息
OutputStream os = socket.getOutputStream();
BufferedWriter bw2 = new BufferedWriter(new OutputStreamWriter(os));
bw2.write("上传成功");
bw2.flush();
socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
// 释放资源
bw2.close();
br.close();
socket.close();
}
客户端
- 创建一个本地字节输入流FileInputStream对象,读取本地文件
- 创建客户端Socket对象
- 使用Socket对象的getOutputStream方法,获取到网络字节输出流
- 通过网络字节输出流传输本地文件内容至服务器(传输结束标记)
- 读取服务端返回的消息
- 释放资源
// 创建本地字节输入流
FileInputStream fis = new FileInputStream("C:\\Users\\Hunter\\Downloads\\xx.md");
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
// 创建客户端Socket对象
Socket socket = new Socket("127.0.0.1", 8888);
// 获取网络字节输出流
OutputStream os = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
// 将本地的文件上传至网络字节输出流
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
}
bw.flush();
socket.shutdownOutput(); // 上传完文件, 给服务端一个结束标记,否则服务端将阻塞
// 获取上传结果
InputStream is = socket.getInputStream();
BufferedReader br2 = new BufferedReader(new InputStreamReader(is));
while ((line = br2.readLine()) != null) {
System.out.println(line);
}
// 释放资源
br.close();
br2.close();
socket.close();
文件上传优化 —— 多线程
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
Socket socket = serverSocket.accept();
/*
* 使用多线程,提高程序效率
* 有一个客户端上传文件,就开启一个线程,完成文件的上传
* */
new Thread(() -> {
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 判断文件加是否存在
File file = new File("C:\\Users\\Hunter\\Downloads");
if (!file.exists()) {
file.mkdirs();
}
// 使用【域名 + 系统时间 + 随机数】用于命名
FileOutputStream fos = new FileOutputStream(file + File.separator + "Hunter" + System.currentTimeMillis() + new Random().nextInt(999999) + ".md");
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
// 将本地上传的文件内容输出至指定文件
String line;
while ((line = br.readLine()) != null) { // 客户端socket.shutdownOutput()的操作,会传来文件的结束标记
bw.write(line);
bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
}
bw.close();
// 返回上传成功的消息
OutputStream os = socket.getOutputStream();
BufferedWriter bw2 = new BufferedWriter(new OutputStreamWriter(os));
bw2.write("上传成功");
bw2.flush();
socket.shutdownOutput(); // 禁用套接字的输出流,同时会传输一个结束标记
// 释放资源
bw2.close();
br.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
进阶
对于文件上传,浏览器将文件以流的形式提交到服务器,使用原生态的文件上传流request.getInputStream()
获取十分麻烦,一般采用apache的开源工具commons-fileupload
,其依赖于commons-io
。
ServletFileUpload类
ServletFileUpload
负责处理上传的文件数据,使用其parseRequest(HttpServletRequest)
方法,会将表单中每个HTML标签封装成一个FileItem
对象,以List的形式返回。
package com.hunter.servlet;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class FileServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 判断上传的文件是否普通表单
if (!ServletFileUpload.isMultipartContent(req)) {
return;
}
// 创建上传文件的保存路径,建议在WEB-INF路径下,安全,用户无法直接访问上传的文件
String uploadPath = this.getServletContext().getRealPath("/WEB-INF/upload");
File uploadFile = new File(uploadPath);
if (!uploadFile.exists()) {
uploadFile.mkdir();
}
// 缓存临时文件
// 临时路径,假如文件超过了预期的大小,就放到临时文件中,超过设定的时间后自动删除,或提醒用户转存为永久
String tmpUploadPath = this.getServletContext().getRealPath("/WEB-INF/tmp");
File tmpUploadFile = new File(tmpUploadPath);
if (!tmpUploadFile.exists()) {
tmpUploadFile.mkdir();
}
/*
* ServletFileUpload负责处理上传的文件数据,使用其parseRequest(HttpServletRequest)方法,
* 会将表单中每个HTML标签封装成一个FileItem对象,以List的形式返回。
* 而使用ServletFileUpload对象解析请求时,需要DiskFileItemFactory对象。
*/
try {
// 1. 创建DiskFileItemFactory对象,处理文件上传 路径或大小限制
DiskFileItemFactory diskFileItemFactory = getDiskFileItemFactory(tmpUploadFile);
// 2. 获取ServletFileUpload
ServletFileUpload servletFileUpload = getServletFileUpload(diskFileItemFactory);
// 3. 处理上传的文件
String msg = uploadParseRequest(req, uploadPath, servletFileUpload);
req.setAttribute("msg", msg);
req.getRequestDispatcher("info.jsp").forward(req, resp);
} catch (FileUploadException e) {
e.printStackTrace();
}
}
private String uploadParseRequest(HttpServletRequest request, String uploadPath, ServletFileUpload servletFileUpload)
throws FileUploadException, IOException {
String msg = "";
// parseRequest(HttpServletRequest)方法,会将表单中每个HTML标签封装成一个FileItem对象,以List的形式返回。
List<FileItem> fileItems = servletFileUpload.parseRequest(request);
for (FileItem fileItem : fileItems) {
// 是否普通表单
if (fileItem.isFormField()) {
// 前端表单控件的name
String fieldName = fileItem.getFieldName();
// UTF-8 处理乱码
String value = fileItem.getString("UTF-8");
System.out.println(fieldName + ": " + value);
} else {
String uploadFileName = fileItem.getName();
// 文件名可能不合法
if (uploadFileName == null || uploadFileName.trim().equals("")) {
continue;
}
// 获得文件名
String fileName = uploadFileName.substring(uploadFileName.lastIndexOf("/") + 1);
System.out.println("文件信息【文件名:" + fileName + "】");
// 使用UUID,保证文件名唯一。网络传输的东西,都需要序列化。
String uuidPath = UUID.randomUUID().toString();
String realPath = uploadPath + "/" + uuidPath;
// 给每个文件创建一个对应的文件夹
File realPathFile = new File(realPath);
if (!realPathFile.exists()) {
realPathFile.mkdir();
}
// 获得文件上传的流
InputStream inputStream = fileItem.getInputStream();
// 创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(realPath + "/" + fileName);
// 创建一个缓冲区
byte[] buffer = new byte[1024 * 1024];
int len = 0;
while ((len = inputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, len);
}
// 关闭流
fileOutputStream.close();
inputStream.close();
msg = "文件上传成功";
// 清除临时文件
fileItem.delete();
}
}
return msg;
}
/**
* 获取ServletFileUpload
*
* @param diskFileItemFactory diskFileItemFactory
* @return ServletFileUpload
*/
private ServletFileUpload getServletFileUpload(DiskFileItemFactory diskFileItemFactory) {
ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
// 监听文件上传进度
servletFileUpload.setProgressListener(
(pBytesRead, pContentLength, pItems) ->
System.out.println("总大小:" + pContentLength + " B,已上传:" + pBytesRead + " B。"));
// 处理乱码问题
servletFileUpload.setHeaderEncoding("UTF-8");
// 设置单个文件的最大值,10MB
servletFileUpload.setFileCountMax(1024 * 1024 * 10);
// 设置总共能上传文件的大小,10MB
servletFileUpload.setSizeMax(1024 * 1024 * 10);
return servletFileUpload;
}
/**
* 创建DiskFileItemFactory对象,处理文件上传 路径或大小限制
*
* @param tmpUploadFile tmpUploadFile
* @return DiskFileItemFactory
*/
private DiskFileItemFactory getDiskFileItemFactory(File tmpUploadFile) {
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
// 通过这个工厂类,设置一个大小为1MB的缓冲区,当上传文件大于这个缓冲区时,放到临时文件中
diskFileItemFactory.setSizeThreshold(1024 * 1024);
diskFileItemFactory.setRepository(tmpUploadFile);
return diskFileItemFactory;
}
}
UDP
java.net.DatagramPacket
:数据报包,包括目的地址和传递的数据
java.net.DatagramSocket
:数据包套接字,通过send方法发送数据报包DatagramPacket对象
发送端
// 1. 建立一个socket
DatagramSocket socket = new DatagramSocket();
// 2. 建一个包
String msg = "教练,我想打球";
InetAddress localhost = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, localhost, 9090);
// 3. 发送包
socket.send(packet);
socket.close();
接收端
// 开放端口
DatagramSocket socket = new DatagramSocket(9090);
// 接收数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
// 3. 接收包
socket.receive(packet);
// 获取发送端的相关信息
System.out.println(packet.getAddress().getHostAddress());
System.out.println(new String(packet.getData(), 0, packet.getLength())); // 实际接收到的数据长度
// 4. 关闭连接
socket.close();
互相聊天
实现了Runnable接口的接收类和发送类:
public class TalkSend implements Runnable {
DatagramSocket socket = null;
BufferedReader reader = null;
private int fromPort;
private String toIp;
private int toPort;
public TalkSend(int fromPort, String toIp, int toPort) {
this.fromPort = fromPort;
this.toIp = toIp;
this.toPort = toPort;
try {
// 1. 建立一个socket
socket = new DatagramSocket(this.fromPort);
// 控制台读取System.in
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
// 2. 建一个包
byte[] msg = reader.readLine().getBytes();
DatagramPacket packet = new DatagramPacket(msg, 0, msg.length,
new InetSocketAddress(this.toIp, this.toPort));
// 3. 发送包
socket.send(packet);
if (new String(msg).equals("bye")) {
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 4. 关闭连接
socket.close();
}
}
public class TalkReceiver implements Runnable {
DatagramSocket socket = null;
private int port;
private String msgFrom;
public TalkReceiver(int port, String msgFrom) {
this.port = port;
this.msgFrom = msgFrom;
try {
// 开放端口
socket = new DatagramSocket(this.port);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
// 接收数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
socket.receive(packet);
// 判断是否断开连接
String receiveData = new String(packet.getData(), 0, packet.getLength());
System.out.println(msgFrom + ": " + receiveData);
if (receiveData.equals("bye")) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
创建有发送和接收信息功能的对象
public class TalkTeacher {
public static void main(String[] args) {
// 发送进程一个端口,接收进程一个端口;发送的目的端口和接收的端口要与聊天的对象对应起来
new Thread(new TalkSend(5555, "localhost", 8888)).start();
new Thread(new TalkReceiver(9999,"student")).start();
}
}
public class TalkStudent {
public static void main(String[] args) {
// 开启两个线程,发送进程一个端口,接收进程一个端口;发送的目的端口和接收的端口要与聊天的对象对应起来
new Thread(new TalkSend(7777, "localhost", 9999)).start();
new Thread(new TalkReceiver(8888, "teacher")).start();
}
}
SMTP与POP3协议
SMTP
发送邮件。
POP3
接收邮件。
MIME
附件。
电子邮件
要在网络上实现邮件功能,必须有专门的邮件服务器。
SMTP服务器地址一般是smtp.xx.com
。
要发送邮件,需要获得协议和支持,开启服务POP3/SMTP
例如网易邮箱: