网络通信的要素
通信双方的地址
通信协议
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没有公有的构造函数,只能通过静态方法来创建实例:
1 2
| InetAddress.getByName(String host); InetAddress.getByAddress(byte[] address);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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);
InetAddress inetAddress3 = InetAddress.getByName("www.baidu.com"); System.out.println(inetAddress3);
System.out.println(inetAddress3.getCanonicalHostName()); System.out.println(inetAddress3.getHostAddress()); System.out.println(inetAddress3.getHostName()); } catch (UnknownHostException e) { e.printStackTrace(); } } }
|
端口
端口表示计算机上一个程序的对外的接口。
1 2 3 4 5 6 7 8 9 10
| 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中读取字节流数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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(); }
|
下载网络资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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(类比:打电话)
需要连接,稳定
三次握手,四次挥手
1 2 3 4 5 6 7 8 9 10
| // 至少需要三次,保证稳定连接 C:我喜欢你 S:我也喜欢你 C:我们在一起吧
// 四次挥手 C:我要走了 S:我知道你要走了 S:你已经走了吗? C:我已经走了
|
有明确的客户端和服务端角色
传输完成后释放连接,效率低
UDP(类比:发短信)
TCP
Socket:套接字,指两台设备之间通讯的端点,是包含了IP地址和端口号的网络单位。
服务端
java.net.ServerSocket
构造方法
ServerSocket(int port)
:创建绑定到指定端口的服务端套接字
成员方法
Socket accept()
:侦听并接收客户端的连接
实现步骤
- 创建服务器端对象ServerSocket,指定端口
- 等待用户的连接,通过accept方法获取请求的客户端对象
- 使用获取到的客户端对象的getInputStream方法获取网络字节输入流InputStream对象
- 使用网络字节输入流对象的read方法读取客户端发来的数据
- 使用获取到的客户端对象的getOutputStream获取网络字节输出流OutputStream对象
- 使用网络字节输出流对象的write方法给客户端发送数据(传输结束标记)
- 释放资源
服务器没有IO流,而是通过获取请求的客户端对象Socket,使用Socket提供的IO流和客户端进行交互:
- 服务器使用客户端的字节输入流InputStream读取客户端发送的数据
- 服务器使用客户端的字节输出流OutputStream向客户端写回数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ServerSocket serverSocket = new ServerSocket(2000); while (true) { Socket socket = serverSocket.accept();
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方法读取服务器传回的数据
- 释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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();
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对象
- 读取客户端上传的文件,写入至服务器中指定的文件夹下
- 获取网络字节输出流对象,返回上传成功的消息(传输结束标记)
- 释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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) { 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方法,获取到网络字节输出流
- 通过网络字节输出流传输本地文件内容至服务器(传输结束标记)
- 读取服务端返回的消息
- 释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| FileInputStream fis = new FileInputStream("C:\\Users\\Hunter\\Downloads\\xx.md"); BufferedReader br = new BufferedReader(new InputStreamReader(fis));
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();
|
文件上传优化 —— 多线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| 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) { 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的形式返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| 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; }
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(); }
try { DiskFileItemFactory diskFileItemFactory = getDiskFileItemFactory(tmpUploadFile); ServletFileUpload servletFileUpload = getServletFileUpload(diskFileItemFactory); 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 = ""; List<FileItem> fileItems = servletFileUpload.parseRequest(request); for (FileItem fileItem : fileItems) { if (fileItem.isFormField()) { String fieldName = fileItem.getFieldName(); 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 + "】");
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; }
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"); servletFileUpload.setFileCountMax(1024 * 1024 * 10); servletFileUpload.setSizeMax(1024 * 1024 * 10); return servletFileUpload; }
private DiskFileItemFactory getDiskFileItemFactory(File tmpUploadFile) { DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
diskFileItemFactory.setSizeThreshold(1024 * 1024); diskFileItemFactory.setRepository(tmpUploadFile); return diskFileItemFactory; } }
|
UDP
java.net.DatagramPacket
:数据报包,包括目的地址和传递的数据
java.net.DatagramSocket
:数据包套接字,通过send方法发送数据报包DatagramPacket对象
发送端
1 2 3 4 5 6 7 8 9 10 11 12
| DatagramSocket socket = new DatagramSocket();
String msg = "教练,我想打球"; InetAddress localhost = InetAddress.getByName("localhost"); DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, localhost, 9090);
socket.send(packet);
socket.close();
|
接收端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
socket.receive(packet);
System.out.println(packet.getAddress().getHostAddress()); System.out.println(new String(packet.getData(), 0, packet.getLength()));
socket.close();
|
互相聊天
实现了Runnable接口的接收类和发送类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 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 { socket = new DatagramSocket(this.fromPort); reader = new BufferedReader(new InputStreamReader(System.in)); } catch (SocketException e) { e.printStackTrace(); } }
@Override public void run() { while (true) { try { byte[] msg = reader.readLine().getBytes(); DatagramPacket packet = new DatagramPacket(msg, 0, msg.length, new InetSocketAddress(this.toIp, this.toPort));
socket.send(packet); if (new String(msg).equals("bye")) { break; } } catch (Exception e) { e.printStackTrace(); } } socket.close(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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(); } } } }
|
创建有发送和接收信息功能的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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
例如网易邮箱: