目录
前言
学习网络编程前,需要有IO流使用基础,以及多线程编程基础。
OSI概述
层 | 模型 | 功能 | TCP/IP四层模型 | 网络协议 |
7 | 应用层 | 程序,软件的运行操作 | 应用层 | HTTP、TFTP, FTP, NFS, WAIS、SMTP |
6 | 表示层 | 对数据的接收进行解释,加密与解密,压缩与解压缩等,也就是把计算机能够识别的东西转换成人能识别的东西,如图片,音频 |
Telnet, Rlogin, SNMP, Gopher | |
5 | 会话层 | 通过传输层(端口号:传输端口与接收端口)建立连接通道。 主要用于接收回话与发送会话请求,设备之间互相认识可以是IP、MAC、主机名。 |
SMTP, DNS | |
4 | 传输层 | 定义了一些传输协议和端口号(如 www 80端口等)。 TCP(传输控制协议)传输效率低,可靠性强,传输数据量大的数据。 UDP(用户数据报协议)与TCP恰恰相反,用于传输可靠性不高,数据量低的数据。如QQ就是UDP协议传输信息。 此层主要是将下层接收数据进行分段和传输,传输数据数据到大目的后再进行重组,长长把这一层叫做段。 |
传输层 | TCP, UDP |
3 | 网络层 | 主要是将下层接收的数据进行IP地址(如192.168.x.x)的封装与解封装。 这一层的工作设备是路由器,常把这一层叫做数据包。 |
网际层 | IP, ICMP, ARP, RARP, AKP, UUCP |
2 | 数据链路层 | 主要是从将物理层接收到的数据进行MAC地址(网卡地址)的封装与解封装。 常把这一层数据叫做帧,这一层的工作设备是交换机 |
数据链路层 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,帧中继 |
1 | 物理层 | 主要定义物理设备标准,如网卡接口类型,光线接口类型,各种传输介质的传输速率等。 主要作用是传输Bit比特流,(也就是由1、0转换为电流强弱进行传输,到达目的后再转换为1、0,这一层的数据称为bit比特流) |
IEEE 802.1A, IEEE 802.2到IEEE 802. |
传输层
主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。
传输层主要使用一下两种协议
- 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
- 用户数据协议-UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
UDP | TCP | |
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
场景 | 适用于实时应用(IP电话、视频会议、直播、文字信息等) | 适用于要求可靠传输的应用,例如文件传输 |
1. 运行在TCP协议上的协议:
HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。
HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
FTP(File Transfer Protocol,文件传输协议),用于文件传输。
POP3(Post Office Protocol, version 3,邮局协议),收邮件用。
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。
TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。
SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。
2. 运行在UDP协议上的协议:
BOOTP(Boot Protocol,启动协议),应用于无盘设备。
NTP(Network Time Protocol,网络时间协议),用于网络同步。
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。
3. 运行在TCP和UDP协议上:
DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。
端口
- 用于标识不同进程(程序)的地址(程序标识)
- 有效端口:0 ~ 65535
- 保留端口:0 ~ 1024。这是系统系统使用的端口。
- 作用:IP地址仅仅代表一台主机,主机与主机之间的进程要想进行通信,进程必须独立监听一个端口。
Socket
Socket就是为网络服务提供的一种机制
- 网络通信其实就是Socket之间的通信
- 通信两段都有Socket
- 数据在两个Socket之间通过IO传输
InetAddress类
此类用来描述IP地址的,并没有构造IP的公共构造方法,因为计算机底层(网络层)本身已经帮我们封装好了,我们要想拿到本机IP与外网IP只需要调用其静态方法获得InetAddress对象。
Inet4Address(IPv4)和Inet6Address(IPv6)对象都继承自InetAddress类
Java网络操作框架,在java.net包
基础常见方法:
- static InetAddress InetAddress.getLocalHost(),获得本主机IP地址对象。
- static InetAddress getByName(String host),直接传递域名,或IP地址构造InetAddress。
- String getHostAddress(),获得IP地址。
- String getHostName(),获得主机名。
package com.bin.demo;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
//获取本机器的IP及主机名
InetAddress my_inetAddress = InetAddress.getLocalHost();
String my_ip = my_inetAddress.getHostAddress();
String my_name = my_inetAddress.getHostName();
System.out.println(my_ip + " ———— " + my_name);
//获取指定域名IP地址及主机名
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
String str_ip = inetAddress.getHostAddress();
String str_name = inetAddress.getHostName();
System.out.println(str_ip + " ———— " + str_name);
//如果不是直接传字符串IP地址,而是直接传域名,其实就是通过设置的DNS服务器对域名解析成IP
my_inetAddress = InetAddress.getByName("localHost"); //DNS解析会去hosts文件查找是否已有解析缓存
my_ip = my_inetAddress.getHostAddress();
my_name = my_inetAddress.getHostName();
System.out.println(my_ip + " ———— " + my_name);
}
}
输出:
192.168.0.108 ———— bin
183.232.231.172 ———— www.baidu.com
127.0.0.1 ———— localHost
Windows 本地 hosts DNS 缓存文件:
UDP协议间的通信
UDP没有客户端和服务端之说,因为UDP不关心是否消息是否被安全送达
UDP发送每个数据报包最大限制64k。
UDP协议相关类
DatagramSocket类
- 此类表示用来发送和接收数据报包的套接字,使用UDP协议。
- send( DatagramPacket p ):发送数据。
- receive( DatagramPacket p ):接收数据。阻塞性方法。
- close():涉及到使用系统IO资源,使用完时关闭资源。
- 不同步的,在多线程中 能用一个DatagramSocket来发送和接收。
- 接收端:必需要为程序构造或设置一个 监听端口
- 发送端 和 接收端:都需要指定一个自身运行程序的 监听端口,不指定则默认使用系统未分配的可用端口,一般只需要设置接收端的 监听端口。
- 一个机器里每个DatagramSocket对象只能监听一个独立的端口,如果一个端口已被其它程序占用,再次绑定时会报java.net.BindException: Address already in use: Cannot bind 异常
常用的构造方法:
DatagramPacket类
- 此类表示数据报包。
- 发送端:必需要构造一个 目的IP + 目的端口
- 数据报包:发送端和接收端都需要一个byte[]数组进行存储数据,这个byte[]将成为缓冲区,也就是UDP不能发超过64K的数据。因为封装的UDP类有缓冲区会自动提取 下一段 数据并发送,所以不必担心。
接收端接收数据时,解包获取数据的常见方法:
- InetAddress getAddress():获取源IP对象。
- int getPort():获取源端口。
- byte[] getData():获取接收到的数据,但数据字节的长度可能 不是 数组的长度,需要 getLength()方法获取准确的数据字节长度。
- int getLength():获取已接收到的数据字节长度。
红框圈起来的是接收端的数据报包构造,其余有设置 IP + port 的都是发送端的数据报包构造。
UDP 发送端
package com.bin.demo;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
// TODO
}
public static void send() throws IOException {
// UDP发送端
DatagramSocket ds = new DatagramSocket();
// 明确数据源
byte[] data = "user = bin; passworld = 7758258;".getBytes("UTF-8"); // 使用UTF-8编码
// 明确目的地IP + port
InetAddress ip = InetAddress.getByName("192.168.0.108"); // 我的本机IP,发给我自己
int port = 21024; // 目地程序监听端口
// 构造数据报包
DatagramPacket dp = new DatagramPacket(data, data.length, ip, port);
// 发送数据报包
ds.send(dp);
// 关闭IO资源
ds.close();
}
}
UDP 接收端 + 发送端
package com.bin.demo;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
receive(); //先启动接收端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(100); //让main线程暂停100ms,防止接收端线程未启动
send();
}
public static void send() throws IOException {
// UDP发送端
DatagramSocket ds = new DatagramSocket();
// 明确数据源
byte[] buf = "user = bin; passworld = 7758258;".getBytes("UTF-8"); // 使用UTF-8编码
// 明确目的地IP + port
InetAddress ip = InetAddress.getByName("192.168.0.108"); // 我的本机IP,发给我自己
int port = 21024; // 目地程序监听端口
// 构造数据报包:封包
DatagramPacket dp = new DatagramPacket(buf, buf.length, ip, port);
// 发送数据报包
ds.send(dp);
// 关闭IO资源
ds.close();
}
public static void receive() throws IOException {
// UDP接收端
DatagramSocket ds = new DatagramSocket(21024); //必须明确监听端口
// 构造接收数据报包容器:解包(对象)
byte[] buf = new byte[1024]; //缓冲区大小
DatagramPacket dp = new DatagramPacket(buf, buf.length);
//接收数据报包
ds.receive(dp); //等待数据:阻塞性方法
//提取数据
String str_ip = dp.getAddress().getHostAddress();
int port = dp.getPort();
// 通过数据解包对象获取byte[]数据,并通过getLength()方法获取已接收的字节长度
String data = new String(dp.getData(), 0, dp.getLength(), "UTF-8"); // 指定编码解码
System.out.println(str_ip + " : " + port + " data--> " + data); //打印输出
//关闭IO资源
ds.close();
}
}
输出:
192.168.0.108 : 59254 data--> user = bin; passworld = 7758258;
UDP 聊天 + 全局广播
这里要实现一个类似对讲机的功能,就是即能发送又能接收的功能。涉及到多线程,一条线程负责发送,一条线程负责接收。
- 255.255.255.255 是局域网全局广播,这样每台机器都能接收到消息。
也可以指定网段网络号 x.x.x. + 最大主机地址,根据不同的子网掩码进行计算。
Phone.java类:用于发送和接收的描述类
package com.bin.demo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class Phone {
private InetAddress sendIP; //发送目的地
private int receivePort; //接收目的地
private class Send implements Runnable {
private DatagramSocket ds;
Send(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //从键盘中读取数据
String line;
try {
while ((line = br.readLine()) != null) { //循环读取,readLine()阻塞性方法,读取一行数据
byte[] buf = line.getBytes("UTF-8"); //指定编码
DatagramPacket dp = new DatagramPacket(buf, buf.length, sendIP, receivePort); //byte[]数据 + 目地IP + 目地port
ds.send(dp); //发送数据
if ("over".equals(line)) { //定义标记,退出聊天
break;
}
}
} catch (IOException e){
// ...
} finally {
if (ds != null && !ds.isClosed()) { //关闭资源
ds.close();
}
}
}
}
private class Receive implements Runnable {
private DatagramSocket ds;
Receive(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
while (true) { //无限循环接收消息
byte[] buf = new byte[1024]; //缓冲区容器大小
DatagramPacket dp = new DatagramPacket(buf, buf.length); //解包对象
ds.receive(dp); //等待消息,阻塞性方法
String ip = dp.getAddress().getHostAddress(); // 获取源IP
int port = dp.getPort(); // 获取源端口
String data = new String(dp.getData(), 0, dp.getLength(), "UTF-8"); // 获取数据
if ("over".equals(data)) { // 读取到有人退出聊天
System.out.println(ip + " : " + port + " [已离线]");
if (InetAddress.getLocalHost().getHostAddress().equals(ip)) { //如果是本机发出的over,接收线程也退出
break;
}
} else {
System.out.println(ip + " : " + port + " ---> " + data); //打印消息
}
}
} catch (IOException e) {
// ...
} finally {
if (ds != null && !ds.isClosed()) { //关闭资源
ds.close();
}
}
}
}
public Phone(String sendIP, int receivePort) throws UnknownHostException { // 构造方法
this.sendIP = InetAddress.getByName(sendIP); // 解析成InetAddress对象
this.receivePort = receivePort; // 接收监听的端口
}
public void start() throws SocketException {
// 构造一个DatagramSocket服务
DatagramSocket ds = new DatagramSocket(receivePort); // 发送和接收的线程都使用一个UDP服务端口
new Thread(new Receive(ds)).start();
new Thread(new Send(ds)).start();
}
}
main.java
package com.bin.demo;
public class Main {
public static void main(String[] args) throws Exception {
Phone phone = new Phone("255.255.255.255", 21024);
phone.start();
}
}
输出:
雷姆
192.168.0.108 : 21024 ---> 雷姆
爱密莉亚
192.168.0.108 : 21024 ---> 爱密莉亚
狂三
192.168.0.108 : 21024 ---> 狂三
over
192.168.0.108 : 21024 [已离线]
UDP发送大文件
思想:
- 发送端:通过不断的发送 UDP 数据包,最后发送一个结束标记。
- 接收端:阻塞式方法 receive(dp) 外部是无线循环,不断地接收数据包,并监听结束标记。
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
receive();
System.out.println("复制完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //mian线程等待200ms,防止接收端未开启
System.out.println("正在发送... ...");
send();
}
public static void send() throws Exception {
//创建UDP发送端对象
DatagramSocket ds = new DatagramSocket();
//目的地
InetAddress sendIP = InetAddress.getByName("192.168.0.108");
int sendPort = 10248;
FileInputStream fis = new FileInputStream("F:\\avi.avi"); //读取要发送的文件
byte[] buf = new byte[1024]; //每次读取的字节缓冲大小
int len = 0; //读取到的字节数
while ((len = fis.read(buf)) != -1) {
// 封装UDP包
DatagramPacket dp = new DatagramPacket(buf, 0, len, sendIP, sendPort);
//发送
ds.send(dp);
}
byte[] flag = "over".getBytes(); //结束标记
ds.send(new DatagramPacket(flag, flag.length, sendIP, sendPort));
//关闭资源
fis.close();
ds.close();
}
public static void receive() throws Exception {
//创建UDP接收端对象,并指定监听一个端口
DatagramSocket ds = new DatagramSocket(10248);
//要存放的目地
FileOutputStream fos = new FileOutputStream("F:\\avi_back.avi");
byte[] buf = new byte[1024]; //接收的容器大小
DatagramPacket dp = new DatagramPacket(buf, buf.length);
while (true) {
ds.receive(dp); //等待消息
byte[] data = dp.getData();
if ("over".equals(new String(data, 0, dp.getLength()))) {
break;
}
fos.write(data, 0, dp.getLength()); //写入
}
//关闭资源
fos.close();
ds.close();
}
}
输出:
正在发送... ...
复制完成
图:
TCP协议间的通信
- TCP面向连接也就是3此握手4次挥手,分客户端和服务端。
- 客户端和服务端建立连接后,都能进行数据的发送和接收
TCP协议相关类
客户端:
Socket类
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
基础构造方法:
基础使用方法:
- getInputStream():返回此套接字的输入流。
- getOutputStream():返回此套接字的输出流。
- shutdownOutputStream():禁用此套接字的输出流。也就是通知服务器此套接字已经输出完成。(因为输出如文件类型的大数据,服务端会一直等待读取,客户端输出完毕时,服务端并不知道),其实就是发送了一个输出结束标记给服务器。
- InetAddress getInetAddress():获取此套接字绑定的IP地址对象。
- boolean isConnected():判断此套接字是否成功连接服务器。
- close():关闭IO流。
服务端:
ServerSocket类
此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
构造方法:
基础使用方法:
- Socket accept():此方法是阻塞性方法,等待客户端连接,返回Socket套接字对象。
- close():服务端一般都是不用关闭的。
Socket访问ServerSocket原理
TCP客户端
package com.bin.demo;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// TODO
}
public static void socket() throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 21024); // 明确连接地址
// 明确数据源,这里发送一条文本数据
byte[] buf = "雷姆".getBytes("UTF-8"); // 指定编码
// 获取输出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 发送数据
// 关闭资源,out字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
s.close();
}
}
TCP服务端 + 客户端
package com.bin.demo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服务端启动...");
serverSocket(); //先启动服务端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main线程暂停200ms,防止服务端未启动
System.out.println("TCP客户端启动...");
socket("雷姆"); //启动客户端
}
public static void socket(String info) throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 21024); // 明确连接地址
// 明确数据源,这里发送一条文本数据
byte[] buf = info.getBytes("UTF-8"); // 指定编码
// 获取输出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 发送数据
// 关闭资源,out字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
s.close();
}
public static void serverSocket() throws IOException {
// TCP服务端
ServerSocket ss = new ServerSocket(21024); // 监听一个端口
// 获取Socket套接字对象
Socket s = ss.accept(); // 阻塞性方法
// 获取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印输出看看是谁连接了服务端
System.out.println(ip + " : " + port + " connection");
// 获取此套接字的读取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024]; // 数据容器
int length = in.read(buf); // 读取数据到容器,并返回读取到数据的字节长度
String data = new String(buf, 0, length, "UTF-8");
//打印输出数据
System.out.println(ip + " : " + port + " data————> " + data);
//关闭资源,in字节读取流本身是Socket套接字所持有,只需关闭套接字
s.close(); //这个套接字是在本服务器内存生成的,所以需要关闭套接字
ss.close(); //关闭服务端套接字
}
}
输出:
TCP服务端启动...
TCP客户端启动...
192.168.0.108 : 51381 connection
192.168.0.108 : 51381 data————> 雷姆
注意:
- 这里服务端获取连接的Socket套接字的读取流时,没有用到循环读取。
- 如果用了循环读取,服务端根本不知道是否已经读取完毕,所以客户端套接字对象需要调用 shutdownOutputStream() 方法发送输出结束标记给服务端,Socket 告诉 ServerSocket 我已输出完成服务端停止读取。
- TCP上传文件例子将运用 shutdownOutputStream() 方法。
TCP客户端与服务端互访
小改一下上面的代码,客户端和服务端,都获取 输出流 和 输入流 通道 并操作。
package com.bin.demo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服务端启动...");
serverSocket(); //先启动服务端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main线程暂停200ms,防止服务端未启动
System.out.println("TCP客户端启动...");
socket("保护雷姆"); //启动客户端
}
public static void socket(String info) throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 21024); // 明确连接地址
// 明确数据源,这里发送一条文本数据
byte[] buf = info.getBytes("UTF-8"); // 指定编码
// 获取输出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 发送数据
// 等待服务端回话
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 关闭资源,out和in字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
s.close();
}
public static void serverSocket() throws IOException {
// TCP服务端
ServerSocket ss = new ServerSocket(21024); // 监听一个端口
// 获取Socket套接字对象
Socket s = ss.accept(); // 阻塞性方法
// 获取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印输出看看是谁连接了服务端
System.out.println(ip + " : " + port + " connection");
// 获取此套接字的读取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024]; // 数据容器
int length = in.read(buf); // 读取数据到容器,并返回读取到数据的字节长度
String data = new String(buf, 0, length, "UTF-8");
//打印输出数据
System.out.println(ip + " : " + port + " data————> " + data);
// 回复客户端
OutputStream out = s.getOutputStream();
out.write("已接收到请求".getBytes("UTF-8"));
//关闭资源,in和out字节读取流本身是Socket套接字所持有,只需关闭套接字
s.close(); //这个套接字是在本服务器内存生成的,所以需要关闭套接字
ss.close(); //关闭服务端套接字
}
}
输出:
TCP服务端启动...
TCP客户端启动...
192.168.0.108 : 53471 connection
192.168.0.108 : 53471 data————> 保护雷姆
已接收到请求
TCP上传文件
- 其实就是文件的Copy过程,客户端读取文件并发送给服务端。
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服务端启动...");
serverSocket(); //先启动服务端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main线程暂停200ms,防止服务端未启动
System.out.println("TCP客户端启动...");
socket(); //启动客户端
}
public static void socket() throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 21024); // 明确连接地址
// 明确数据源,上传文件
FileInputStream file_in = new FileInputStream("F:\\test.txt");
// 获取输出通道
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024]; //每次读取的字节容器
int file_length; //读取到的字节数
while ((file_length = file_in.read(buf)) != -1) {
out.write(buf, 0, file_length); //写入读取到的字节长度
}
// 发送输出结束标记
s.shutdownOutput();
// 等待服务端回话
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 关闭资源,out和in字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
file_in.close(); //关闭Copy文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服务端
ServerSocket ss = new ServerSocket(21024); // 监听一个端口
// 获取Socket套接字对象
Socket s = ss.accept(); // 阻塞性方法
// 获取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印输出看看是谁连接了服务端
System.out.println(ip + " : " + port + " connection");
// 获取此套接字的读取通道 + 明确源文件存放目地
InputStream in = s.getInputStream();
long time = System.currentTimeMillis(); // 获取当前时间戳设置文件名,避免文件重名或被覆盖问题
FileOutputStream file_out = new FileOutputStream("F:\\test_" + ++time +".txt"); //存放目的地
byte[] buf = new byte[1024]; // 每次读取的数据容器
int length; // 读取到数据的字节长度
while ((length = in.read(buf)) != -1) {
file_out.write(buf, 0, length);
}
// 回复客户端
OutputStream out = s.getOutputStream();
out.write("上传成功".getBytes("UTF-8"));
//关闭资源,in和out字节读取流本身是Socket套接字所持有,只需关闭套接字
file_out.close(); //关闭写出文件流
s.close(); //这个套接字是在本服务器内存生成的,所以需要关闭套接字
ss.close(); //关闭服务端套接字
}
}
输出:
TCP服务端启动...
TCP客户端启动...
192.168.0.108 : 54457 connection
上传成功
TCP下载文件
- 其实就是文件的Copy过程,客户端请求服务端文件并下载。
package com.bin.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服务端启动...");
serverSocket(); //先启动服务端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main线程暂停200ms,防止服务端未启动
System.out.println("TCP客户端启动...");
socket(); //启动客户端
}
public static void socket() throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 10248); // 明确连接地址
// 请求的文件路径
String file = new String("F:\\test.txt");
// 获取输出通道
OutputStream out = s.getOutputStream();
out.write(file.getBytes("UTF-8")); //发送请求
// 等待服务端返回数据
InputStream in = s.getInputStream();
FileOutputStream file_out = new FileOutputStream("F:\\下载_test.txt");
byte[] inBuf = new byte[1024]; //每次读取的字节大小
int length;
while ((length = in.read(inBuf)) != -1) {
file_out.write(inBuf, 0, length);
}
System.out.println("下载完成");
// 关闭资源,out和in字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
file_out.close(); //关闭写出文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服务端
ServerSocket ss = new ServerSocket(10248); // 监听一个端口
// 获取Socket套接字对象
Socket s = ss.accept(); // 阻塞性方法
// 获取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印输出看看是谁连接了服务端
System.out.println(ip + " : " + port + " connection");
// 获取此套接字的读取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int length = in.read(buf);
File file = new File(new String(buf, 0, length));
if (file.exists()) { //检查服务器是否存在该文件
// 回复客户端
OutputStream out = s.getOutputStream(); //获取输出通道
FileInputStream file_in = new FileInputStream(file);
byte[] file_buf = new byte[1024];
int file_len;
while ((file_len = file_in.read(file_buf)) != -1) {
out.write(file_buf, 0, file_len);
}
// 发送一个输出结束标记
file_in.close(); //关闭读取文件流
}
//关闭资源,in和out字节读取流本身是Socket套接字所持有,只需关闭套接字
s.close(); //这个套接字是在本服务器内存生成的,所以需要关闭套接字
ss.close(); //关闭服务端套接字
}
}
输出:
TCP服务端启动...
TCP客户端启动...
192.168.0.108 : 55706 connection
下载完成
TCP多客户端并发访问服务器
- 服务器一般都是不需要直接关闭的,而是持久的运行着。
- 这里服务端只需要为每个已连接Socket套接字开启新线程,就OK了。+ 服务器无限循环。
这里用了TCP上传文件的例子示范
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服务端启动...");
serverSocket(); //先启动服务端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main线程暂停200ms,防止服务端未启动
for (int i = 0; i < 10; ++i) { //创建10条线程并发访问服务器,上传文件
new Thread() {
@Override public void run() {
try {
socket(); //启动客户端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
public static void socket() throws IOException {
// TCP客户端
Socket s = new Socket("192.168.0.108", 21024); // 明确连接地址
// 明确数据源,上传文件
FileInputStream file_in = new FileInputStream("F:\\test.txt");
// 获取输出通道
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024]; //每次读取的字节容器
int file_length; //读取到的字节数
while ((file_length = file_in.read(buf)) != -1) {
out.write(buf, 0, file_length); //写入读取到的字节长度
}
// 发送输出结束标记
s.shutdownOutput();
// 等待服务端回话
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 关闭资源,out和in字节输出流对象本身是Socket套接字所持有,所以只需关闭套接字
file_in.close(); //关闭Copy文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服务端
ServerSocket ss = new ServerSocket(21024); // 监听一个端口
// 无限循环
while (true) {
// 获取Socket套接字对象
Socket s = ss.accept(); // 阻塞性方法
// 服务端只需要为每个已连接Socket套接字开启新线程,就OK了
new Thread(new Task(s)).start();
}
}
static class Task implements Runnable {
private Socket s;
private static int count; //文件计数,防止文件被覆盖
Task(Socket s) {
this.s = s;
}
@Override public void run() {
try {
// 获取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印输出看看是谁连接了服务端
System.out.println(ip + " : " + port + " connection");
// 获取此套接字的读取通道 + 明确源文件存放目地
InputStream in = s.getInputStream();
long time = System.currentTimeMillis(); // 获取当前时间戳设置文件名,避免文件重名或被覆盖问题
FileOutputStream file_out = new FileOutputStream("F:\\test_" + time + "_" + ++count + ".txt"); //存放目的地
byte[] buf = new byte[1024]; // 每次读取的数据容器
int length; // 读取到数据的字节长度
while ((length = in.read(buf)) != -1) {
file_out.write(buf, 0, length);
}
// 回复客户端
OutputStream out = s.getOutputStream();
out.write("上传成功".getBytes("UTF-8"));
//关闭资源,in和out字节读取流本身是Socket套接字所持有,只需关闭套接字
file_out.close(); //关闭写出文件流
s.close(); //这个套接字是在本服务器内存生成的,所以需要关闭套接字
} catch (Exception e) {
// ...
}
}
}
}
输出:
TCP服务端启动...
192.168.0.108 : 56845 connection
192.168.0.108 : 56848 connection
192.168.0.108 : 56846 connection
192.168.0.108 : 56847 connection
192.168.0.108 : 56849 connection
上传成功
上传成功
上传成功
192.168.0.108 : 56850 connection
192.168.0.108 : 56852 connection
上传成功
上传成功
192.168.0.108 : 56851 connection
192.168.0.108 : 56853 connection
192.168.0.108 : 56854 connection
上传成功
上传成功
上传成功
上传成功
上传成功
TCP几个小问题
- 避免上传或下载重名问题,文件会被覆盖
- Socket套接字上传文件已到末尾结束时,必须调用 Socket套接字的 shutdownOutput() 方法发送输出结束标记给服务端
- Socket下载服务端文件时,服务端输出完成无需调用 Socket 套接字的 shutdownOutput() 方法。
- 多客户端并发访问服务器时,为每个Socket套接字单独用创建一条线程运行。
- 如果一个客户端已连接服务端,但某个时刻客户端挂掉了,服务端不知道,这时候服务端会有计时器,每隔一段时间发送消息到已挂掉的客户端判断是否还在,达到一定次数客户端未回应后,服务端就会自动关闭这个已挂掉的套接字资源。
浏览器与Tomcat
浏览器访问服务器基本原理
- 封装了Socket的程序都是客户端(如浏览器,不同厂商的浏览器)
- 封装了ServerSocket的程序都为服务器(如Tomcat)。
- html文件包含需要的资源信息会继续发送请求(也就是并不是我们表面上看到请求一个html页面时只发送一个请求)
- 不指定资源则会默认返回默认资源(index.html)
- 指定资源请求时可能还会附加请求参数(用户名、密码)
- 浏览器其实就是封装了Socket,服务器封装了ServerSocket
模拟服务器接收浏览器发送的HTTP协议请求消息
package com.bin.demo;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// 模拟服务器
ServerSocket ss = new ServerSocket(9090);
System.out.println("服务器启动 ...");
// 获取连接对象
Socket s = ss.accept();
System.out.println(s.getInetAddress().getHostAddress() + " : " + s.getPort() + " connection"); // 打印谁连接了服务器
// 读取请求HTTP协议信息
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int length = in.read(buf);
String str = new String(buf);
System.out.println(str); // 打印请求的HTTP协议信息
// 关闭资源
s.close();
ss.close();
}
}
输出:
服务器启动 ...
192.168.0.108 : 51853 connection
GET / HTTP/1.1
Host: 192.168.0.108:9090
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
浏览器发送的HTTP协议请求信息图解:
模拟浏览器接收服务器返回的HTTP协议响应消息
package com.bin.demo;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// 模拟浏览器
Socket s = new Socket("www.baidu.com", 80); // web资源的域名,一般默认为80端口
// 发送请求
PrintWriter out = new PrintWriter(s.getOutputStream(), true); //使用打印流,第二个参数的自动刷新
out.println("GET / HTTP/1.1");
out.println("Host: " + s.getInetAddress().getHostAddress() +":80");
out.println("Connection: close");
out.println("Accept: */*"); //告诉服务器我支持的文件查看类型,这里使用通配符告诉服务器我支持全部类型的文件
out.println("Accept-Language: zh");
out.println();
// 接收返回的HTTP响应协议信息
InputStream in = s.getInputStream();
byte[] buf = new byte[3024]; //这里只读取3024,自己调调也可以
int length = in.read(buf);
System.out.println(new String(buf, 0, length, "UTF-8")); //国际标准的UTF-8编码解码方式
// 关闭资源
s.close();
}
}
这里只提取一部分HTTP响应内容:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
Content-Type: text/html
Date: Sun, 05 Apr 2020 17:07:16 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Pragma: no-cache
Server: BWS/1.1
Set-Cookie: BAIDUID=F42521B806B3D4A92243E42336518FED:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=F42521B806B3D4A92243E42336518FED; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1586106436; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BAIDUID=F42521B806B3D4A9493E1FA4E62E6E4D:FG=1; max-age=31536000; expires=Mon, 05-Apr-21 17:07:16 GMT; domain=.baidu.com; path=/; version=1; comment=bd
Traceid: 1586106436030523265010215562608745898825
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1
Connection: close
<!DOCTYPE html><!--STATUS OK-->
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<link rel="dns-prefetch" href="//s1.bdstatic.com"/>
<link rel="dns-prefetch" href="//t1.baidu.com"/>
<link rel="dns-prefetch" href="//t2.baidu.com"/>
<link rel="dns-prefetch" href="//t3.baidu.com"/>
<link rel="dns-prefetch" href="//t10.baidu.com"/>
<link rel="dns-prefetch" href="//t11.baidu.com"/>
<link rel="dns-prefetch" href="//t12.baidu.com"/>
<link rel="dns-prefetch" href="//b1.bdstatic.com"/>
<title>百度一下,你就知道</title>
<link href="http://s1.bdstatic.com/r/www/cache/static/home/css/index.css" rel="stylesheet" type="text/css" />
<!--[if lte IE 8]><style index="index" >#content{height:480px\9}#m{top:260px\9}</style><![endif]-->
<!--[if IE 8]><style index="index" >#u1 a.mnav,#u1 a.mnav:visited{font-family:simsun}</style><![endif]-->
<script>var hashMatch = document.location.href.match(/#+(.*wd=[^&].+)/);if (hashMatch && hashMatch[0] && hashMatch[1]) {document.location.replace("http://"+location.host+"/s?"+hashMatch[1]);}var ns_c = function(){};</script>
<script>function h(obj){obj.style.behavior='url(#default#homepage)';var a = obj.setHomePage('//www.baidu.com/');}</script>
<noscript><meta http-equiv="refresh" content="0; url=/baidu.html?from=noscript"/></noscript>
<script>window._ASYNC_START=new Date().getTime();</script>
</head>
简单的图解:
资源定位类
URL类
URL链接地址字符串的解析对象
基本的方法:
- getProtocol():获取协议
- getHost():获取IP地址
- getPort():获取端口
- getFile():获取资源路径及请求参数
- getQuery():只获取参数
- URLConnection openConnection():创建返回一个URLConnection连接对象,调用此方法将会进行连接,它代表应用程序和 URL 之间的通信链接。此类的实例可用于读取和写入此 URL 引用的资源(重要方法)
package com.bin.demo;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception {
//创建URL对象
URL url = new URL("http://192.168.0.108:9090/myapp/1.html?user=bin&password=520"); //请求这个页面时携带数据参数
//基本方法,解析URL中的数据
System.out.println("getProtocol : " + url.getProtocol()); //获取协议
System.out.println("getHost : " + url.getHost());
System.out.println("getPort : " + url.getPort());
System.out.println("getFile : " + url.getFile()); //获取资源路径及请求参数
System.out.println("getQuery : " + url.getQuery()); //获取参数
}
}
输出:
getProtocol : http
getHost : 192.168.0.108
getPort : 9090
getFile : /myapp/1.html?user=bin&password=520
getQuery : user=bin&password=520
URLConnection类
此类封装了Socket,也就是能和服务器进行读写操作的类。
它代表应用程序和 URL 之间的通信链接。此类的实例可用于读取和写入此 URL 引用的资源。
此类是抽象类,通过URL对象创建URLConnection连接对象
基础常用方法:
- OutputStream getOutputStream():写出数据到此资源定位路径。
- InputStream getInputStream():读取此定位资源。
- URL getURL():返回URL对象。
package com.bin.demo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
public class Main {
public static void main(String[] args) throws Exception {
//创建URL对象
URL url = new URL("http://www.baidu.com");
//获得URL连接对象
URLConnection conn = url.openConnection();
//读取此定位资源
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String str = null;
while ((str = in.readLine()) != null) {
System.out.println(str);
}
}
}
输出(这里是被解析后的应答体数据):
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
注意:
返回的数据已经帮我们解析好了,读取服务器返回的HTTP协议消息应答头已被解析,甚至连应答体中包含的资源信息也被解析了,所以给程序员返回的只有数据内容。要想拿到应答头调用相应方法即可。
URLConnection 下载图片实战
基础常用方法:
- setRequestProperty(String key, String value):设置一般请求属性。如果已存在具有该关键字的属性,则用新值改写其值。设置的一般请求属性取决于协议。
常用方法(连接成功(URL调用openConnection()方法)后可调用的方法):
- OutputStream getOutputStream():写出数据到此资源定位路径。
- InputStream getInputStream():读取此定位资源。
- String getContentEncoding():获取数据的压缩方式,如gzip、deflate、compress
- String getContentType():获取内容数据类型,如文件的类型。
- int getContentLength():获取响应体内容数据的长度,为int类型,单位字节byte。
- long getContentLengthLong():同getContentLength()方法,这里返回long类型。
- long getLastModifed():获取此资源的上一次修改时间,单位为毫秒值。
- String getHeaderField(String key):获取返回的响应头信息。
package com.bin.demo;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) throws Exception {
//创建URL对象
URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1586181338511&di=0ef74d62555781d08e5a0968b397c75e&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2F83025aafa40f4bfb1209aa1d0b4f78f0f6361899.jpg");
//获得URL连接对象
URLConnection conn = url.openConnection();
//获取参数
String http = conn.getHeaderField(null); // 获取第一行应答头信息,也就是协议头
String encoding = conn.getContentEncoding();
String type = conn.getContentType();
int length = conn.getContentLength();
long lengthLong = conn.getContentLengthLong();
long motifyTime = conn.getLastModified();
System.out.println("协议头 :" + http);
System.out.println("数据压缩方式 :" + encoding);
System.out.println("类型 :" + type);
System.out.println("int字节长度 :" + length);
System.out.println("long自己长度 :" + lengthLong);
System.out.println("上一次修改时间 :" + new SimpleDateFormat("yyyy年MM/dd日 k:m:s:S").format(new Date(motifyTime)));
System.out.println("——————————————————");
//确定存放路径
FileOutputStream out = new FileOutputStream("F:\\LeiMu.jpg");
//读取此定位资源
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
byte[] buf = new byte[1024]; //每次读取的字节数
int len = 0;
long count = 0; //判断文件是否下载成功的统计字节数(根据真实内容的字节大小 对比 已下载的内容字节大小)
while ((len = in.read(buf)) != -1) {
count += len;
out.write(buf, 0, len);
}
//关闭下载Copy资源
out.close();
// 判断是否下载完成,(数据是否完整)
if (count == lengthLong) {
System.out.println("下载成功");
}
}
}
输出:
协议头 :HTTP/1.1 200 OK
数据压缩方式 :null
类型 :image/jpeg
int字节长度 :44160
long自己长度 :44160
上一次修改时间 :2016年08/14日 19:51:29:0
——————————————————
下载成功
下面是一些常见的Content-Type
字段的值:
- text/plain
- text/html
- text/css
- image/jpeg
- image/png
- image/svg+xml
- audio/mp4
- video/mp4
- application/javascript
- application/pdf
- application/zip
- application/atom+xml
HttpURLConnection类
- 此类是抽象类并继承自URLConnection类,通养通过URL的openConnection()获取 URLConnection对象并强转为HttpURLConnection对象。
- 此类支持 HTTP 特定功能的 URLConnection。
常见基础方法:
setRequestMethod(String method):设置 URL 请求的方法,以上方法之一是合法的,具体取决于协议的限制。默认方法为 GET。
- GET
- POST
- HEAD
- OPTIONS
- PUT
- DELETE
- TRACE
- int getResponseCode():获取返回的状态码。
- String getRequestMethod():获取当前请求的方法。
- String getRequestMessage():获取返回的状态码描述信息。
- disconnect() : 关闭连接。
静态常量状态码,还有很多没截图完:
package com.bin.demo;
import java.net.HttpURLConnection;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception {
//创建URL对象
URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1586181338511&di=0ef74d62555781d08e5a0968b397c75e&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2F83025aafa40f4bfb1209aa1d0b4f78f0f6361899.jpg");
//获得URL连接对象
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
String method = conn.getRequestMethod(); //请求方法
int code = conn.getResponseCode(); //响应状态码
String codeInfo = conn.getResponseMessage(); //状态码描述信息
System.out.println(method); //GET
System.out.println(code); //200
System.out.println(codeInfo); //OK
conn.disconnect(); //关闭连接
}
}
输出:
GET
200
OK
作用
Socket 和 ServerSocket:
- 能拿到请求与应答(头与体)消息的全部消息,但并没有进一步的资源信息解析
- Socket 接收 ServerSocket 返回的应答体也就是源码
URLConnection对象(返回的数据已经帮我们解析好了):
- 读取服务器返回的HTTP协议消息应答头已被解析,甚至连应答体中包含的资源信息也被解析了,所以给程序员返回的只有数据内容。
- 想要得到应答头内容,只需要调用相应API方法。
HttpURLConnection:
- 看名字就知道是基于HTTP的,加入了响应码,以及请求参数的设置如 GET POST 方法。
Socket ,URLConnection,HttpURLConnection 这几个对象同样能进行从网络数据的发送与接收,同样可以进行图片的下载。
网络架构
C/S
Client Server (客户端服务器)
- 客户端和服务端都需要编写
- 客户端需要维护
- 客户端可以分担运算
如大型运算的网络游戏,3D建模,技能特效。
B/S
Browser Server (浏览器服务器)
- 只需要编写服务端,(客户端就是浏览器)
- 客户端是不必关心的,不需要维护的
- 运算全在服务器端
HTTP
HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
HTTP响应模型
模型 | 简介作用 |
单进程I/O模型 | 服务端开启一个进程,一个进程仅能处理一个请求,并且对请求顺序处理 |
多进程I/O模型 | 服务端并行开启多个进程,同样的一个进程只能处理一个请求,这样服务端就可以同时处理多个请求 |
复用I/O模型 | 服务端开启一个进程,但是呢,同时开启多个线程,一个线程响应一个请求,同样可以达到同时处理多个请求,线程间并发执行 |
复用多线程I/O模型 | 服务端并行开启多个进程,同时每个进程开启多个线程,这样服务端可以同时处理进程数M*每个进程的线程数N个请求。 |
协议版本
协议版本 | 简介作用 |
HTTP/0.9 | HTTP协议的最初版本,功能简陋,仅支持请求方式GET,并且仅能请求访问HTML格式的资源。 |
HTTP/1.0 | 在0.9版本上做了进步,增加了请求方式POST和HEAD;不再局限于0.9版本的HTML格式,根据Content-Type可以支持多种数据格式。 但是1.0版本的工作方式是每次TCP连接只能发送一个请求,当服务器响应后就会关闭这次连接,下一个请求需要再次建立TCP连接,就是不支持Connection: keep-alive |
HTTP/1.1 | 1.1 版的最大变化,就是引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明 客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送 |
HTTP/2.0 | 为了解决1.1版本利用率不高的问题,提出了HTTP/2.0版本。增加双工模式,即不仅客户端能够同时发送多个请求,服务端也能同时处理多个请求,解决了队头堵塞的问题(HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级)并以压缩的方式传输,提高利用率。 当前主流的协议版本还是HTTP/1.1版本。 |
HTTP/1.0与HTTP.1.1比较图:
通用协议标头
可以出现在请求标头和响应标头中。
字段 | 作用 |
Date | 返回的值为距离格林威治标准时间 1970 年 1 月 1 日的毫秒数。 |
Cache-Control | 有四个参数:
|
Connection | Connection 决定当前事务(一次三次握手和四次挥手)完成后,是否会关闭网络连接。Connection 有两种,一种是
|
HTTP1.1 其他通用标头如下:
实体协议标头
实体标头是描述消息正文内容的 HTTP 标头。实体标头用于 HTTP 请求和响应中。
字段 | 作用 |
Content-Length | 体报头指示实体主体的大小,以字节为单位,发送到接收方。 |
Content-Language | 实体报头描述了客户端或者服务端能够接受的语言 |
Content-Encoding | 这个实体报头用来压缩媒体类型。Content-Encoding 指示对实体应用了何种编码。
|
实体标头字段图:
请求协议标头
字段 | 作用 | ||||||||||
Host | Host 请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号。如果没有给定端口号,会自动使用被请求服务的默认端口(比如请求一个 HTTP 的 URL 会自动使用80作为端口)。 | ||||||||||
Referer | HTTP Referer 属性是请求标头的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。 | ||||||||||
Upgrade-Insecure-Requests | Upgrade-Insecure-Requests 是一个请求标头,用来向服务器端发送信号,表示客户端优先选择加密及带有身份验证的响应。如:Upgrade-Insecure-Requests: 1 | ||||||||||
If-Modified-Since | HTTP 的 If-Modified-Since 使其成为条件请求:
If-Modified-Since 通常会与 If-None-Match 搭配使用,If-Modified-Since 用于确认代理或客户端拥有的本地资源的有效性。获取资源的更新日期时间,可通过确认首部字段 Last-Modified 来确定。 大白话说就是如果在 Last-Modified 之后更新了服务器资源,那么服务器会响应200,如果在 Last-Modified 之后没有更新过资源,则返回 304。 |
||||||||||
If-None-Match | If-None-Match HTTP请求标头使请求成为条件请求。 对于 GET 和 HEAD 方法,仅当服务器没有与给定资源匹配的 ETag 时,服务器才会以200状态发送回请求的资源。 对于其他方法,仅当最终现有资源的ETag 与列出的任何值都不匹配时,才会处理请求。 |
||||||||||
Accept | 告知服务端,客户端都安装了那些媒体软件,可接收那种数据类型 文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml 图片文件: image/jpeg、image/gif、image/png 视频文件: video/mpeg、video/quicktime 应用程序二进制文件: application/octet-stream、application/zip 如:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 q 表示的是权重,媒体类型增加优先级,没有显示权重的时候默认值是1.0 :
这是一个放置顺序,权重高的在前,低的在后 |
||||||||||
Accept-Charset | 规定服务器处理表单数据所使用的字符集。 常用的字符集有: UTF-8 - Unicode 字符编码 ; |
||||||||||
Accept-Language | 告知服务器用户代理能够处理的自然语言 |
基于 HTTP 1.1:
响应协议标头
字段 | 作用 |
Access-Control-Allow-Origin | 一个返回的 HTTP 标头可能会具有 Access-Control-Allow-Origin ,Access-Control-Allow-Origin 指定一个来源,它告诉浏览器允许该来源进行资源访问。 否则-对于没有凭据的请求 *通配符,告诉浏览器允许任何源访问资源。例如,要允许源 https://mozilla.org 的代码访问资源 |
Keep-Alive | Keep-Alive 表示的是 Connection 非持续连接的存活时间,如下: Connection: Keep-Alive Keep-Alive: timeout=5, max=997 有两个参数:
|
Server | 服务器使用的软件的信息。 |
Set-Cookie | 服务器返回的Cookie认证标识 |
Transfer-Encoding | 首部字段 Transfer-Encoding 规定了传输报文主体时采用的编码方式。 |
X-Frame-Options | 首部字段 X-Frame-Options 属于 HTTP 响应首部,用于控制网站内容在其他 Web 网站的 Frame 标签内的显示问题。其主要目的是为了防止点击劫持(clickjacking)攻击。 |
基于 HTTP 1.1:
状态码
状态码类别:
类别 | 原因短语 |
1XX | Informational(信息性状态码) 接受的请求正在处理 |
2XX | Success(成功状态码) 请求正常处理完毕 |
3XX | Redirection(重定向状态码) 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) 服务器处理请求出错 |
常用HTTP状态码:
2XX | 成功(这系列表明请求被正常处理了) |
200 | OK,表示从客户端发来的请求在服务器端被正确处理 |
204 | No content,表示请求成功,但响应报文不含实体的主体部分 |
206 | Partial Content,进行范围请求成功 |
3XX | 重定向(表明浏览器要执行特殊处理) |
301 | moved permanently,永久性重定向,表示资源已被分配了新的 URL |
302 | found,临时性重定向,表示资源临时被分配了新的 URL |
303 | see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源(对于301/302/303响应,几乎所有浏览器都会删除报文主体并自动用GET重新请求) |
304 | not modified,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关) |
307 | temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求 |
4XX | 客户端错误 |
400 | bad request,请求报文存在语法错误 |
401 | unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 |
403 | forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述 |
404 | not found,表示在服务器上没有找到请求的资源 |
5XX | 服务器错误 |
500 | internal sever error,表示服务器端在执行请求时发生了错误 |
501 | Not Implemented,表示服务器不支持当前请求所需要的某个功能 |
503 | service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 |
常用请求方法
HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。
- GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。
- POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。
- PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。
- DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。
HTTP与HTTPS的区别
区别 | HTTP | HTTPS |
协议 | 运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份 | 身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, 是添加了加密和认证机制的 HTTP。 |
端口 | 80 | 443 |
资源消耗 | 较少 | 由于加解密处理,会消耗更多的 CPU 和内存资源 |
开销 | 无需证书 | 需要证书,而证书一般需要向认证机构购买 |
加密机制 | 无 | 共享密钥加密和公开密钥加密并用的混合加密机制 |
安全性 | 弱 | 由于加密机制,安全性强 |
Session、Cookie和Token
HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。
什么是cookie:
cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。
什么是session:
session是依赖Cookie实现的。session是服务器端对象
session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。
cookie与session区别:
- 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
- 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制
- 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。
什么是Token:
Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。
session与token区别:
- session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
- session存储在服务器端,token存储在客户端
- token提供认证和授权功能,作为身份认证,token安全性比session好;
- session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)
深入学习参考:重学TCP/IP协议和三次握手四次挥手