基本知识
OSI与TCP/IP参考模型
1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫比特。
2. 数据链路层:主要将从物理层接受的数据进行MAC地址(网卡地址,网卡可以配置IP地址和物理地址。物理地址就是网卡出厂是带着一个编号,这个编号全球唯一。)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。
3. 网络层:主要将从下层接收到的数据进行IP地址(例192.168.0.1)的封装与解封装。这一层封装完就知道了我们的数据到底要发向哪一台主机。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。交换机实现了互联,路由器实现了数据包方向的定义。
4. 传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据按照传输协议进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。。
5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起回话或者接受会话请求(设备之间需要互相认识,可以是IP也可以是MAC或者是主机名)
6. 表示层:主要是进行对接收的数据进行解释、加密、与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够是别的东西(如图片、声音等))。明确到底是什么类型的数据。
7. 应用层:主要是一些终端的应用,比如FTP(各种文件下载),WEB(IE浏览),QQ之类的(可以把它理解成我们在电脑屏幕上可以看到的东西,就是终端应用)。
当我们进行网络传输的时候,数据在源处从应用层到物理层,这个过程叫封包。然后数据通过网络传输到目的地,在目的地处数据从物理层到应用层,这个过程叫拆包。这样就完成了数据在网络的传输。
网络通讯要素
IP地址
如果两台电脑想要进行通讯,就必须要先找到对方。想要找到对方,就要给每台计算机一个标识,类似于电话号码,这就是IP地址。
IP地址是一个32位的二进制数,通常被分割成4个8位,就是4个字节,每个字节的范围都是0~255。所以IP地址的数量是有限制的,这种4个字节来表示的IP地址叫做IPv4,它已经在2013年被全部分配完毕。后期便出现了IPv6版本的IP地址,扩大了地址空间。
IP地址分为A、B、C、D、E5类,它们适用的类型分别为:大型网络;中型网络;小型网络;多目地址;备用。常用的是B和C两类。
其中A、B、C类中又有一部分IP地址被单独列出来,专门为科研等领域使用,这种IP地址类型叫做私有地址。
类别 | 最大网络数 | IP地址范围 | 最大主机数 | 私有IP地址范围 |
---|---|---|---|---|
A | 126(2^7-2) | 0.0.0.0-127.255.255.255 | 16777214 | 10.0.0.0-10.255.255.255 |
B | 16384(2^14) | 128.0.0.0-191.255.255.255 | 65534 | 172.16.0.0-172.31.255.255 |
C | 2097152(2^21) | 192.0.0.0-223.255.255.255 | 254 | 192.168.0.0-192.168.255.255 |
广播地址:是专门用于同时向网络中所有工作站进行发送的一个地址。255就是广播地址。
在IP地址段走C类地址的话,子网掩码是255.255.255.0得话,IP地址的前三位就是网络位,第四位是IP地址位,0~255,0不能用,0代表的网络位就是192.168.1.0网段。IP地址从1有效到254,255不是IP地址,255是广播地址。就是说如果发到192.168.1.255上就是发给192.168.1网段所有存活的机器上
本地回环地址:不属于任何一个有类别地址类。它代表设备的本地虚拟接口,所以默认被看作是永远不会宕掉的接口。在windows操作系统中也有相似的定义,所以通常在不安装网卡前就可以ping通这个本地回环地址。一般都会用来检查本地网络协议、基本数据接口等是否正常的。
因为IP地址不容易记忆,根据实际情况给电脑起一个名字,就是
主机名,方便记忆。IP地址和主机名都是一台计算机的标识。
localhost:本地主机
端口号
如果想要从一台电脑A的QQ发消息给电脑B的QQ,首先要通过B的IP地址(假设IP地址是192.168.1.1)找到这台电脑,然后电脑A的QQ发送数据,电脑B收到数据后应该解析数据,解析是靠应用程序来解析,但是电脑B上有许多应用程序,怎么分别到底找哪个程序来解析?这就需要端口号了,它给每个程序都加上一个数字标识,比如QQ这个软件的标识是4000,所以电脑A的QQ发消息时就是发到192.168.1.1地址的4000应用程序上,标识为192.168.1.1:4000。
有效端口范围:0~65535,其中0~1024是系统使用或保留端口,一般不占用。
传输协议
在我们进行数据传输的过程中,要按照什么样的方式进行数据的传递?这就是传输协议,数据传输的规则。
UDP数据报文协议
1. 将数据及源和目的封装成数据包中,然后直接丢向目的地,不需要建立连接
2. 每个数据报的大小在限制在64k内
3. 因无连接,是不可靠协议。数据包已经丢掉,但是对方有可能并没有收到。
4. 不需要建立连接,速度快
TCP传输控制协议
1. 建立连接,形成传输数据的通道。如果对方不在则不进行数据传输
2. 在连接中进行大数据量传输
3. 通过三次握手完成连接,是可靠协议
4. 必须建立连接,效率会稍低
IP地址
IP地址的内容比较复杂,并且还有与之相对应的主机名,所以在Java中IP地址被封装成了对象。
类InetAddress表示互联网协议(IP)地址。有两个子类Inet4Address(IPv4)和Inet6Address(IPv6),其中IPv6是16进制的。
它处于参考模型的网际层。
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println("name="+ip1.getHostName());
System.out.println("IP="+ip1.getHostAddress());
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println("百度IP="+ip1.getHostAddress());
输出结果:
name=huhao-PC
IP=192.168.1.102
百度IP=192.168.1.102
获取InetAddress对象的方法有许多。其中getByName方法可以通过主机名或者IP地址来获取IP地址对象。所以可以通过这个方法来获得百度、新浪等网站主机的IP地址,不过这些网站有的IP地址不唯一,因为他们是服务器集群。
域名解析
如果我们想访问某一个网站的话,无非是网站的一台计算机上存储了新浪的网页数据,而这个主机一定有一个IP地址,所以我们想访问它的主机的话就要访问这个IP地址,直接在浏览器输入IP地址就能访问了。但是互联网上的IP地址非常多,我们不可能记住每一个IP地址,而想要确定一个计算机的地址,除了使用IP地址之外还可以使用主机名。所以我们只要记住这个主机名就可以了。
IP地址和主机名就产生了对应关系,在互联网上有许多公共服务器,在他们上面就存放着这些IP地址和主机名的对应关系,如果我们在浏览器输入www.sina.com一回车,就会先去这些公共服务器上的列表内寻找IP地址和主机名的对应关系,然后获取IP地址后再通过IP地址来连接新浪主机,这就是域名解析。
这些公共服务器就是DNS:域名解析服务器
如果我们不在网卡设置中指定DNS,那么宽带服务商会帮我们指定一个DNS,我们的信息会发给宽带服务商,然后他帮我们转到DNS上。
如果想要提高解析速度,我们可以本地解析,可以在本机创建一张域名解析列表,其实不是我们创建的,计算机内本身就有,我们可以在里面自己配置一些域名解析列表。就是hosts文件。这样解析速度会快,因为域名解析的时候最先走的不是域名解析服务器,而是本地hosts文件。
传输协议
UDP传输协议
如果要进行网络传输,就必须要有两个端点,每个端点存在后如果想要进行通讯就要有一个Socket,可以把Socket理解成通讯的两端,数据在Socket之间进行传输。
Socket:套接字。套接字是两台机器间通信的端点。
UDP是一种传输协议,我们没有办法直接对其进行操作,于是Java建立了一些对象,对UDP进行了封装,方便我们操作。UDP传输协议对应的端点服务对象就是DatagramSocket类。它表示用来发送和接收数据报包的套接字。它既可以发送也可以接收。发送和接收的都是数据报包,数据报包中有多信息,所以Java把数据报包也封装成了对象,就是DatagramPacket类。数据报包用来实现无连接的包投递服务。
数据报包在构造的时候是有区别的,因为DatagramSocket既可以发送也可以接收,所以有一下数据报包是用来发送的,有一些是用来接收,因为发送是需要目的地的,所以只要构造函数参数中包含IP地址对象,就是发送包。
//发送
public class UDPSendDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
byte[] buf = "发送端on".getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(), 10000);
ds.send(dp);
System.out.println("发送成功");
ds.close();
}
}
//接收
public class UDPRecDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10000);
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
ds.receive(dp);
String ip = dp.getAddress().getHostAddress();
int port = dp.getPort();
String text = new String(dp.getData(),0,dp.getLength());
System.out.println(ip+":"+port+":"+text);
ds.close();
}
}
输出结果:
//发送端
发送成功
//接收端
192.168.1.102:64594:发送端on
这里的端口号是使用发送端的数据包中封装的方法获得IP地址对象,所以获得的是发送端的端口,不是接收端的端口,接收端的端口是10000,因为发送端没有指定端口,所以就随机了一个端口,发送端不用管是什么端口,只要把包的目的端口明确,目的地不发错,接收端也只负责接收发给我的数据,哪里发的不用管。
receive方法是阻塞式方法,如果发送端的数据没有发来,接收端会一直等待。
TCP传输协议
TCP协议的解释与使用
如果我们想使用TCP协议进行传输,就需要两个对象,Socket(客户端套接字)和ServerSocket(服务器套接字)。既然是客户端与服务端,所以客户端一般不止一个。
客户端创建流程:
1. 首先建立Socket对象,建立对象的时候构造函数可以使用IP对象+端口或IP地址的字符串+端口来明确要连接的主机(或者使用空参数构造函数,就是通过系统默认的SocketImpl来创建未连接的Socket对象,再使用connect方法来连接主机,connect方法的参数是一个IP地址和端口的封装)。当Socke对象创建完成,就代表着服务端与客户端连接建立成功,数据传输通道已建立。
2. 数据传输通道就是Socket流,它既有输入又有输出,因为是网络传输所以不只是文本,应该是字节流,分别通过Socket类的getInputStream和getOutputStream方法来获取。
3. 通过字节输出流向Socket流中写数据
4. 关闭资源
服务端创建流程:
1. 创建ServerSocket对象。
2. 服务端和客户端想要连接,服务端相当于开启了一个应用程序来提供服务,我们需要的只是这个服务,如果客户端想要连接到服务端,需要明确服务端的IP地址和这个应用程序的端口,所以服务端的ServerSocket对象需要通过构造函数对外暴露端口。
3. 如果我们想给客户端传输数据,查看ServerSocket类,发现它没有流。服务端其实并不需要流,因为服务端上面存的都是数据,我们只要能够把这些数据返回给客户端就行了,只要可以做到返回给客户端,他自己就不需要流。因为服务端只有一个,而客户端有可能会有多个, 当多个客户端同时连到主机发送数据的时候,主机为了明确A客户端的数据就是返回给A客户端而不是B客户端的,所以它如果想和A客户端进行通信,就会取得A客户端上的socket流,用A的流来和A通信,这样肯定就不会乱了。所以需要获取A的socket对象。
4. 通过A的socket对象获取它的输入输出流,使用A客户端的读取流读取客户端发来的数据,使用A客户端的输出流发送给A客户端数据
5. 关闭资源。
示例1:客户端与服务端之间进行数据传输
//先运行服务端再运行客户端
public class ClientDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("192.168.1.100",10004);
OutputStream out = socket.getOutputStream();
out.write("客户端发送的数据".getBytes());
System.out.println("客户端已发送数据");
socket.shutdownOutput();//告诉服务端,客户端已经写完了
//读取服务端的返回数据
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!=-1){
System.out.println(new String(buf, 0, len));
}
socket.close();
}
}
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10004);
Socket s = ss.accept();
InputStream in = s.getInputStream();
String ip = s.getInetAddress().getHostAddress();
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!=-1){
String text = new String(buf, 0, len);
System.out.println(ip+":"+text);
}
//服务器给客户端返回值
OutputStream out = s.getOutputStream();
out.write("服务器返回的数据".getBytes());
System.out.println("服务器已返回数据");
s.close();
ss.close();
}
}
输出结果:
//客户端
客户端已发送数据
服务器返回的数据
//服务端
192.168.1.100:客户端发送的数据
服务器已返回数据
示例2:客户端输入字母,发给服务端,服务端变成大写返回给客户端
//客户端
public class TransClient {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws IOException {
Socket s = new Socket("192.168.1.100",10005);
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader bufrIn = new BufferedReader(new InputStreamReader(s.getInputStream()));
String line = null;
while((line = bufr.readLine())!=null){
if(line.equals("over"))
break;
out.write(line+LINE_SEPARATOR);
out.flush();
System.out.println("客户端输入:"+line);
//读取服务端返回的数据
String text = bufrIn.readLine();
System.out.println("客户端收到:"+text);
}
bufr.close();
s.close();
}
}
//服务端
public class TransServer {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10005);
Socket s = ss.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line = null;
while((line = in.readLine())!=null){
System.out.println("服务端收到:"+line);
String str = line.toUpperCase();
out.write(str+LINE_SEPARATOR);
out.flush();
System.out.println("服务端发出:"+str);
}
s.close();
ss.close();
}
}
注意:
1. 客户端输入over后服务端也结束了,因为客户端输入over后while循环break,然后执行s.close方法,就在socket流中植入了结束标记,所以服务端的readLine方法就读到了-1,返回null,于是服务端程序也就结束了
2. 如果程序发生了等待,那一定是有阻塞式方法没有结束造成的。有时是因为数据写到缓冲区之后没有刷新出去,所以对方读不到造成等待,这种就要加刷新。或者是对readLine方法来说,读到换行标记才算是读完了一行,也就是说加换行就可以了。
示例3:客户端从文本文件中读取,然后发送给服务端,服务端变成大写之后保存到另一个文本文件中,也就是上传。
//客户端
public class UploadClient {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws IOException {
File file = new File("E:\\upload.txt");
BufferedReader bufr = new BufferedReader(new FileReader(file));
Socket s = new Socket("192.168.1.100",10006);
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line = null;
while((line = bufr.readLine())!=null){
out.write(line+LINE_SEPARATOR);
out.flush();
}
//告诉服务端已经发送完了
s.shutdownOutput();
//接收服务端的返回内容
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
String str = in.readLine();
System.out.println(str);
bufr.close();
s.close();
}
}
//服务端
public class UploadServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10006);
Socket s = ss.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
File file = new File("E:\\upload1.txt");
BufferedWriter bufw = new BufferedWriter(new FileWriter(file));
String line = null;
while((line = in.readLine())!=null){
String str = line.toUpperCase();
bufw.write(str);
bufw.newLine();
bufw.flush();
}
//将上传结果返回给客户端
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
out.write("上传成功");
out.flush();
s.close();
ss.close();
bufw.close();
}
}
注意:当客户端的循环结束后,服务端的循环没有结束,因为服务端读的是socket流,发来一句就度一句,然后写到缓冲区,当客户端读完后,客户端readLine返回null,客户端知道内容已经读完了,但是服务端不知道,所以服务端的循环就一直在等待了。这时候就要告诉服务端已经读完了,就是用shutdownOutput方法。
如果服务端把数据写到缓冲区没有刷新,服务端会有数据,大小是8k,这是缓冲区BufferedWriter默认大小,装满后就不再装了,然后会内容刷新出去,如果第二次没有装满,就不会自动刷新,所以即使不加刷新,服务端也会有数据,但是数据大小不对,缺内容。
示例4:服务端多线程技术
如果我们想上传一张图片,按照示例3的代码来写就会产生一个问题,同时只能有一个客户端上传,当一个服务区获取到一个客户端的socket之后其他的客户端就不能连上来了,只有处理完这个客户端之后才能继续连接其他的客户端。要解决这个问题就要用到多线程。
//客户端
public class UploadPicClient {
public static void main(String[] args) throws IOException{
Socket s = new Socket("192.168.1.100",10007);
FileInputStream fis = new FileInputStream("E:\\0.jpg");
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf))!=-1){
out.write(buf, 0, len);
out.flush();
}
//告诉服务器数据已写完
s.shutdownOutput();
//读取服务器返回的数据
InputStream in = s.getInputStream();
byte[] bufIn = new byte[1024];
int lenIn = 0;
while((lenIn = in.read(bufIn))!= -1){
System.out.println(new String(bufIn,0,lenIn));
}
s.close();
fis.close();
}
}
//服务端
public class UploadPicServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10007);
while(true){
Socket s = ss.accept();
new Thread(new UploadTask(s)).start();
}
}
}
//封装线程任务
public class UploadTask implements Runnable {
private Socket s;
public UploadTask(Socket s) {
super();
this.s = s;
}
public void run() {
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+".....connect");
try {
int count = 1;
InputStream in = s.getInputStream();
//把上传的图片都保存在一个文件夹中,如果不存在则创建
File dir = new File("e:\\pic");
if(!dir.exists()){
dir.mkdirs();
}
//如果多次上传同名文件,则文件名后加括号数字
File file = new File(dir,ip+".jpg");
while(file.exists()){
file = new File(dir, ip+"("+count+")"+".jpg");
count++;
}
FileOutputStream fos = new FileOutputStream(file);
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!=-1){
fos.write(buf, 0, len);
}
//服务器收到后给客户端返回
OutputStream out = s.getOutputStream();
out.write("服务器已收到".getBytes());
//服务端不用关,因为在不断循环接收客户端的连接
s.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
常见客户端与服务端
我们最常用的客户端就是IE浏览器,服务端就是Tomcat。
我们平时使用浏览器的时候是在浏览器地址栏输入网页地址,比如输入地址http://www.sina.com.cn/index.html,回车后会先通过DNS获取到服务器的IP地址,然后通过IP地址访问服务器,服务器装有提供web服务的软件比如Tomcat,如果没有指定端口的话, 默认的WEB服务都是80端口,但Tomcat默认的端口是8080端口。这样客户端就找到了服务器上提供服务的应用程序,就可以开始进行数据传输了。
因为客户端IE浏览器和服务端提供服务的软件Tomcat是不同的厂商制作的,如果他们之间想要进行数据的通讯,就要遵循同一个规则,就是http超文本传输协议,它是应用层的一种通讯协议,它使用html超文本标记语言。它定义了web浏览器和web服务端的通讯规则,浏览器和服务器两端都要遵循这个协议,web服务器对外提供了超文本数据,浏览器解析超文本数据。客户端按照http协议进行主机的访问,所以客户端根据http协议要求的内容和格式开始发送请求消息,这些请求消息就是请求报文。
当客户端上发来请求的数据后,Tomcat就是服务器处理请求和应答的应用程序。Tomcat服务器对外提供了接口,我们按照他的接口规则间接或直接实现就可以,这个接口就是Servlet接口。
服务端如果有数据,就会开始读取数据并把应答数据根据http协议发给客户端的浏览器,这里的应答数据就是响应报文。浏览器本身已经内置了能够解析http协议的解析程序,我们称之为解析引擎。所以就开始解析,然后我们就看到了网页上的内容。浏览器与服务器底层数据传输用的就是Socket技术。
请求报文
如果我们想查看客户端浏览器在向服务器发送的具体内容,我们可以自己做一个Tomcat服务器来接收浏览器发送的数据
public class MyTomcat {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10008);
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"....connect");
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
//给客户端一个反馈
PrintWriter out = new PrintWriter(s.getOutputStream(),true);//记得刷新
out.println("欢迎光临");
ss.close();
s.close();
}
}
运行程序,在IE浏览器访问http://192.168.1.100:10008,输出结果:
192.168.1.110....connect
GET / HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
UA-CPU: AMD64
Accept-Encoding: gzip, deflate
Host: 192.168.1.110:10008
Connection: Keep-Alive
请求报文主要分成三部分,请求行、请求头部和请求数据。
请求行:请求方法有GET和POST两种。请求方法后面有/myweb/1.html请求的资源路径,再后面是协议版本,http主要有两个版本1.0和1.1。
请求消息头:分为属性名:属性值,就是键值对。它的存在是为了在遵守http协议的基础上告诉服务器一些信息。
Accept:客户端可识别的内容类型列表。
Accept-Language:支持的语言
Accept-Encoding:支持的压缩方式,如果一个网页的内容较多,就可以先在服务器端压缩,然后发送到客户端再解压缩。
User-Agent:一些系统信息
Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。
Connection:Keep-Alive保持存活
请求体:如果我们想向服务器发送数据的时候带着自己的东西,就要卸载请求体。请求体和请求头必须分开,中间有一个空行。
响应报文
当Tomcat服务器收到客户端浏览器发来的请求后,会给出响应,如果我们想知道响应的具体内容,可以自己做一个浏览器应用程序来查看Tomcat返回的信息。
public class MyBrowser {
public static void main(String[] args) throws IOException {
Socket s = new Socket("192.168.1.110",8080);
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
out.println("GET /myweb/1.html HTTP/1.1");
out.println("Accept: */*");
out.println("Accept-Language: zh-CN");
out.println("Accept-Encoding: gzip, deflate");
out.println("Host: 192.168.1.110:10008");
out.println("Connection: close");
out.println();
out.println();
InputStream in = s.getInputStream();
byte[] buf = new byte[1024*1024];
int len = in.read(buf);
String str =new String(buf,0,len);
System.out.println(str);
s.close();
}
}
运行Tomcat服务器,输出结果:
HTTP/1.1 200 OK
Accept-Ranges: bytes
ETag: W/"10-1499136276522"
Last-Modified: Tue, 04 Jul 2017 02:44:36 GMT
Content-Type: text/html
Content-Length: 10
Date: Tue, 04 Jul 2017 06:30:02 GMT
Connection: close
<html>
<head>
<title>这是我的网页</title>
</head>
<body>
<h1>欢迎光临</h1>
<font size='5' color="red">这是一个tomcat服务器中的资源。是一个html网页。</font>
</body>
</html>
注意:向Tomcat服务器写入内容后面有两个空行,第一个空行是请求消息头和请求体中间的空行,第二个空行是请求体
服务端发回的是应答消息,分为应答行、应答消息头,就是属性名:属性值的键值对和应答体。
应答行:应答行的内容包括http的协议版本,应答状态码,应答状态描述信息。这些内容和浏览器服务器都没什么关系,是http协议规定的内容。
状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
常见状态代码、状态描述的说明如下。
200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
应答消息:一些必要的属性信息
应答体:网页中的具体内容
URL和URLConnection
URL和URI
我们在浏览器中查看源文件是只有应答体的,但是我们自己做的浏览器myBrowser却显示全部应答报文。这是因为当服务器根据http协议返回应答消息后,浏览器由于是内置了http解析引擎的,所以他就把应答体解析出来然后显示在我们面前。而我们现在用的是自己写的浏览器MyBrowser,它没有解析引擎,所以就把应答报文全部显示出来了。所以我们需要一个可以解析http的东西。
我们以前操作的Socket、DatagramSocket都是在传输层,而http协议是在应用层。我们在浏览器输入网页地址的地址是一个很复杂的东西,于是Java就帮我们封装成了对象,这就是java.net包中的URL类和URI类。
URL:统一资源定位符。定位就是指能根据这个符号定位到网络上的一个资源(可以是网页资源文件或目录)。这里说的符号不是指单一符号,而是指http://192.168.1.100:8080/myweb/1.html这一串符号。所以URL里面是包含协议的。
URI:统一资源标识符。凡是能标识统一资源的符号都是URI。URI 是统一资源标识符,而 URL 是统一资源定位符。因此,笼统地说,每个 URL 都是 URI,但不一定每个 URI 都是 URL。这是因为 URI 还包括一个子类,即统一资源名称 (URN),它命名资源但不指定如何定位资源。
String str_url = "http://192.168.1.110:8080/myweb/1.html?name=lisi";
URL url = new URL(str_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("getPath:"+url.getPath());
System.out.println("getQuery:"+url.getQuery());
输出结果:
getProtocol:http
getHost:192.168.1.110
getPort:8080
getFile:/myweb/1.html?name=lisi
getPath:/myweb/1.html
getQuery:name=lisi
?name=lisi是URL的查询部分Query,就是URL的参数信息,如果不加这部分,File和Path是一样的,但如果加上参数信息,文件会负责到参数,但是路径只负责到文件。
如果我们想只获取应答体,就要使用URL的openStream方法,打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。
它可以按照地址连出去,返回连接后的读取流。连上之后说明Socket流就有了,然后拿到读取流,就可以读取资源的数据
String str_url = "http://192.168.1.110:8080/myweb/1.html?name=lisi";
URL url = new URL(str_url);
InputStream in = url.openStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
String text = new String(buf,0,len);
System.out.println(text);
in.close();
再运行就只拿到了应答体,因为URL帮我们按照http协议解析了
解析原理:
其实openStream方法调用的是openConnection().getInputStream()
openConnection()返回的是URLConnection,URL连接器对象,它能连接到指定的统一资源定位符指向的资源上,将连接封装成了对象。如果我们想知道连接器中的内容,可以打印对象进行查看
String str_url = "http://192.168.1.110:8080/myweb/1.html?name=lisi";
URL url = new URL(str_url);
URLConnection conn = url.openConnection();
System.out.println(conn);
输出结果:
sun.net.www.protocol.http.HttpURLConnection:http://192.168.1.110:8080/myweb/1.html?name=lisi
http协议包中的HttpURLConnection,这个包是sun公司给我们提供的软件包,是一种底层实现,它帮我们封装了http的解析方式。URLConnection conn这个对象是通过openConnection()方法获取到的,我们new不了,包没有对外提供,这个对象就可以解析Tomcat服务器发回来的数据。
它还有方法能获取到InputStream和OutputStream,能拿这两个流的只有Socket。其实URL连接器对象就是用的就是socket流,再加上协议。
所以InputStream in = url.openStream();的原理其实是
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();