1.网络编程入门
1.1 网络编程概述
- 计算机网络
是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统 - 网络编程
在网络通信协议下,不同计算机上运行的程序,可以进行数据传输
1.2 网络编程三要素
- IP地址
要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识 - 端口
网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识 - 协议
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议
1.3 IP地址
IP地址:是网络中设备的唯一标识
- IP地址分为两大类
- IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多
- IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,这样就解决了网络地址资源数量不够的问题
- DOS常用命令:
- ipconfig:查看本机IP地址
- ping IP地址:检查网络是否连通
- 特殊IP地址:
- 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用
1.4 InetAddress
InetAddress:此类表示Internet协议(IP)地址
相关方法
| 方法名 | 说明 | | —- | —- | | static InetAddress getByName(String host) | 确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址 | | String getHostName() | 获取此IP地址的主机名 | | String getHostAddress() | 返回文本显示中的IP地址字符串 |代码演示
public class InetAddressDemo {
public static void main(String[] args) throws UnknownHostException {
//InetAddress address = InetAddress.getByName("itheima");
InetAddress address = InetAddress.getByName("192.168.1.66");
//public String getHostName():获取此IP地址的主机名
String name = address.getHostName();
//public String getHostAddress():返回文本显示中的IP地址字符串
String ip = address.getHostAddress();
System.out.println("主机名:" + name);
System.out.println("IP地址:" + ip);
}
}
1.5 端口和协议
- 端口
- 设备上应用程序的唯一标识
- 端口号
- 用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
- 协议
- 计算机网络中,连接和通信的规则被称为网络通信协议
- UDP协议
- 用户数据报协议(User Datagram Protocol)
- UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
- 由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
- 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议
- TCP协议
- 传输控制协议 (Transmission Control Protocol)
- TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
- 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠
第一次握手,客户端向服务器端发出连接请求,等待服务器确认
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求
第三次握手,客户端再次向服务器端发送确认信息,确认连接 - 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等
2.UDP通信程序
2.1 UDP发送数据
- Java中的UDP通信
- UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念
- Java提供了DatagramSocket类作为基于UDP协议的Socket
构造方法
| 方法名 | 说明 | | —- | —- | | DatagramSocket() | 创建数据报套接字并将其绑定到本机地址上的任何可用端口 | | DatagramPacket(byte[] buf,int len,InetAddress add,int port) | 创建数据包,发送长度为len的数据包到指定主机的指定端口 |相关方法
| 方法名 | 说明 | | —- | —- | | void send(DatagramPacket p) | 发送数据报包 | | void close() | 关闭数据报套接字 | | void receive(DatagramPacket p) | 从此套接字接受数据报包 |发送数据的步骤
- 创建发送端的Socket对象(DatagramSocket)
- 创建数据,并把数据打包
- 调用DatagramSocket对象的方法发送数据
- 关闭发送端
代码演示
public class SendDemo {
public static void main(String[] args) throws IOException {
//创建发送端的Socket对象(DatagramSocket)
// DatagramSocket() 构造数据报套接字并将其绑定到本地主机上的任何可用端口
DatagramSocket ds = new DatagramSocket();
//创建数据,并把数据打包
//DatagramPacket(byte[] buf, int length, InetAddress address, int port)
//构造一个数据包,发送长度为 length的数据包到指定主机上的指定端口号。
byte[] bys = "hello,udp,我来了".getBytes();
DatagramPacket dp = new DatagramPacket(bys,bys.length,InetAddress.getByName("127.0.0.1"),10086);
//调用DatagramSocket对象的方法发送数据
//void send(DatagramPacket p) 从此套接字发送数据报包
ds.send(dp);
//关闭发送端
//void close() 关闭此数据报套接字
ds.close();
}
}
2.2UDP接收数据
- 接收数据的步骤
- 创建接收端的Socket对象(DatagramSocket)
- 创建一个数据包,用于接收数据
- 调用DatagramSocket对象的方法接收数据
- 解析数据包,并把数据在控制台显示
- 关闭接收端
构造方法
| 方法名 | 说明 | | —- | —- | | DatagramPacket(byte[] buf, int len) | 创建一个DatagramPacket用于接收长度为len的数据包 |相关方法
| 方法名 | 说明 | | —- | —- | | byte[] getData() | 返回数据缓冲区 | | int getLength() | 返回要发送的数据的长度或接收的数据的长度 |示例代码
public class ReceiveDemo {
public static void main(String[] args) throws IOException {
//创建接收端的Socket对象(DatagramSocket)
DatagramSocket ds = new DatagramSocket(12345);
//创建一个数据包,用于接收数据
byte[] bys = new byte[1024];
DatagramPacket dp = new DatagramPacket(bys, bys.length);
//调用DatagramSocket对象的方法接收数据
ds.receive(dp);
//解析数据包,并把数据在控制台显示
System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
}
}
}
2.3UDP通信程序练习
- 案例需求
UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收 代码实现 ```java /* UDP发送数据:
数据来自于键盘录入,直到输入的数据是886,发送数据结束
*/ public class SendDemo { public static void main(String[] args) throws IOException {
//创建发送端的Socket对象(DatagramSocket) DatagramSocket ds = new DatagramSocket(); //键盘录入数据 Scanner sc = new Scanner(System.in); while (true) { String s = sc.nextLine(); //输入的数据是886,发送数据结束 if ("886".equals(s)) { break; } //创建数据,并把数据打包 byte[] bys = s.getBytes(); DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("192.168.1.66"), 12345); //调用DatagramSocket对象的方法发送数据 ds.send(dp); } //关闭发送端 ds.close();
} }
/ UDP接收数据: 因为接收端不知道发送端什么时候停止发送,故采用死循环接收 / public class ReceiveDemo { public static void main(String[] args) throws IOException { //创建接收端的Socket对象(DatagramSocket) DatagramSocket ds = new DatagramSocket(12345); while (true) { //创建一个数据包,用于接收数据 byte[] bys = new byte[1024]; DatagramPacket dp = new DatagramPacket(bys, bys.length); //调用DatagramSocket对象的方法接收数据 ds.receive(dp); //解析数据包,并把数据在控制台显示 System.out.println(“数据是:” + new String(dp.getData(), 0, dp.getLength())); } //关闭接收端 // ds.close(); } }
<a name="J8ZsD"></a>
## 2.4UDP三种通讯方式
- 单播<br />单播用于两个主机之间的端对端通信
- 组播<br />组播用于对一组特定的主机进行通信
- 广播<br />广播用于一个主机对整个局域网上所有主机上的数据通信
![02_UDP单播02.png](https://cdn.nlark.com/yuque/0/2022/png/22908640/1648945188616-94feeb3d-214f-42f1-9b58-fa8d4b26158f.png#clientId=u82e3cedf-23a3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=323&id=ub84b7103&name=02_UDP%E5%8D%95%E6%92%AD02.png&originHeight=404&originWidth=922&originalType=binary&ratio=1&rotation=0&showTitle=false&size=83570&status=done&style=none&taskId=u925918b1-a4c4-482c-851e-88eaddc2339&title=&width=737.6)<br />![04_UDP组播02.png](https://cdn.nlark.com/yuque/0/2022/png/22908640/1648945202633-2d1339f0-f08d-4960-9ba5-492aee97b728.png#clientId=u82e3cedf-23a3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=322&id=u42db4e85&name=04_UDP%E7%BB%84%E6%92%AD02.png&originHeight=402&originWidth=928&originalType=binary&ratio=1&rotation=0&showTitle=false&size=83747&status=done&style=none&taskId=ubb68a0b4-4a4f-4e22-a8a4-c9b4b8cf265&title=&width=742.4)<br />![06_UDP广播02.png](https://cdn.nlark.com/yuque/0/2022/png/22908640/1648945223241-9071f5ff-0bf8-4f7f-83ee-f2f63019ca0c.png#clientId=u82e3cedf-23a3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=346&id=u08b2b7e1&name=06_UDP%E5%B9%BF%E6%92%AD02.png&originHeight=432&originWidth=937&originalType=binary&ratio=1&rotation=0&showTitle=false&size=88255&status=done&style=none&taskId=u17b796d7-38db-42a0-b5ed-1a775c42a62&title=&width=749.6)
<a name="CgkFU"></a>
## 2.5UDP组播实现
- 实现步骤
- 发送端
1. 创建发送端的Socket对象(DatagramSocket)
1. 创建数据,并把数据打包(DatagramPacket)
1. 调用DatagramSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址)
1. 释放资源
- 接收端
1. 创建接收端Socket对象(MulticastSocket)
1. 创建一个箱子,用于接收数据
1. 把当前计算机绑定一个组播地址
1. 将数据接收到箱子中
1. 解析数据包,并打印数据
1. 释放资源
- 代码实现
```java
// 发送端
public class ClinetDemo {
public static void main(String[] args) throws IOException {
// 1. 创建发送端的Socket对象(DatagramSocket)
DatagramSocket ds = new DatagramSocket();
String s = "hello 组播";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.1.0");
int port = 10000;
// 2. 创建数据,并把数据打包(DatagramPacket)
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
// 3. 调用DatagramSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址)
ds.send(dp);
// 4. 释放资源
ds.close();
}
}
// 接收端
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建接收端Socket对象(MulticastSocket)
MulticastSocket ms = new MulticastSocket(10000);
// 2. 创建一个箱子,用于接收数据
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
// 3. 把当前计算机绑定一个组播地址,表示添加到这一组中.
ms.joinGroup(InetAddress.getByName("224.0.1.0"));
// 4. 将数据接收到箱子中
ms.receive(dp);
// 5. 解析数据包,并打印数据
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data,0,length));
// 6. 释放资源
ms.close();
}
}
2.6UDP广播实现
- 实现步骤
- 发送端
- 创建发送端Socket对象(DatagramSocket)
- 创建存储数据的箱子,将广播地址封装进去
- 发送数据
- 释放资源
- 接收端
- 创建接收端的Socket对象(DatagramSocket)
- 创建一个数据包,用于接收数据
- 调用DatagramSocket对象的方法接收数据
- 解析数据包,并把数据在控制台显示
- 关闭接收端
- 发送端
- 代码实现
// 发送端 public class ClientDemo { public static void main(String[] args) throws IOException { // 1. 创建发送端Socket对象(DatagramSocket) DatagramSocket ds = new DatagramSocket(); // 2. 创建存储数据的箱子,将广播地址封装进去 String s = "广播 hello"; byte[] bytes = s.getBytes(); InetAddress address = InetAddress.getByName("255.255.255.255"); int port = 10000; DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port); // 3. 发送数据 ds.send(dp); // 4. 释放资源 ds.close(); } } // 接收端 public class ServerDemo { public static void main(String[] args) throws IOException { // 1. 创建接收端的Socket对象(DatagramSocket) DatagramSocket ds = new DatagramSocket(10000); // 2. 创建一个数据包,用于接收数据 DatagramPacket dp = new DatagramPacket(new byte[1024],1024); // 3. 调用DatagramSocket对象的方法接收数据 ds.receive(dp); // 4. 解析数据包,并把数据在控制台显示 byte[] data = dp.getData(); int length = dp.getLength(); System.out.println(new String(data,0,length)); // 5. 关闭接收端 ds.close(); } }
3.TCP通信程序
3.1TCP发送数据
- Java中的TCP通信
- Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
- Java为客户端提供了Socket类,为服务器端提供了ServerSocket类
构造方法
| 方法名 | 说明 | | —- | —- | | Socket(InetAddress address,int port) | 创建流套接字并将其连接到指定IP指定端口号 | | Socket(String host, int port) | 创建流套接字并将其连接到指定主机上的指定端口号 |相关方法
| 方法名 | 说明 | | —- | —- | | InputStream getInputStream() | 返回此套接字的输入流 | | OutputStream getOutputStream() | 返回此套接字的输出流 |示例代码
public class ClientDemo { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket s = new Socket("127.0.0.1",10000); //获取输出流,写数据 //OutputStream getOutputStream() 返回此套接字的输出流 OutputStream os = s.getOutputStream(); os.write("hello,tcp,我来了".getBytes()); //释放资源 s.close(); } }
3.2TCP接收数据
构造方法
| 方法名 | 说明 | | —- | —- | | ServletSocket(int port) | 创建绑定到指定端口的服务器套接字 |相关方法
| 方法名 | 说明 | | —- | —- | | Socket accept() | 监听要连接到此的套接字并接受它 |注意事项
- accept方法是阻塞的,作用就是等待客户端连接
- 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
- 针对客户端来讲,是往外写的,所以是输出流
针对服务器来讲,是往里读的,所以是输入流 - read方法也是阻塞的
- 客户端在关流的时候,还多了一个往服务器写结束标记的动作
- 最后一步断开连接,通过四次挥手协议保证连接终止
- 三次握手和四次挥手
- 三次握手
- 四次挥手
- 三次握手
示例代码
public class ServerDemo { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket ss = new ServerSocket(10000); //Socket accept() 侦听要连接到此套接字并接受它 Socket s = ss.accept(); //获取输入流,读数据,并把数据显示在控制台 InputStream is = s.getInputStream(); byte[] bys = new byte[1024]; int len = is.read(bys); String data = new String(bys,0,len); System.out.println("数据是:" + data); //释放资源 s.close(); ss.close(); } }
3.3TCP程序练习
案例需求
客户端:发送数据,接受服务器反馈
服务器:收到消息后给出反馈- 案例分析
- 客户端创建对象,使用输出流输出数据
- 服务端创建对象,使用输入流接受数据
- 服务端使用输出流给出反馈数据
- 客户端使用输入流接受反馈数据
代码实现
// 客户端 public class ClientDemo { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1",10000); OutputStream os = socket.getOutputStream(); os.write("hello".getBytes()); // os.close();如果在这里关流,会导致整个socket都无法使用 socket.shutdownOutput();//仅仅关闭输出流.并写一个结束标记,对socket没有任何影响 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line; while((line = br.readLine())!=null){ System.out.println(line); } br.close(); os.close(); socket.close(); } } // 服务器 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); Socket accept = ss.accept(); InputStream is = accept.getInputStream(); int b; while((b = is.read())!=-1){ System.out.println((char) b); } System.out.println("看看我执行了吗?"); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("你谁啊?"); bw.newLine(); bw.flush(); bw.close(); is.close(); accept.close(); ss.close(); } }
3.4TCP程序文件上传练习
- 案例需求
客户端:数据来自于本地文件,接收服务器反馈
服务器:接收到的数据写入本地文件,给出反馈 - 案例分析
- 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
- 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
- 客户端接受服务端的回馈信息
相关方法
| 方法名 | 说明 | | —- | —- | | void shutdownInput() | 将此套接字的输入流放置在“流的末尾” | | void shutdownOutput() | 禁止用此套接字的输出流 |代码实现
// 客户端 public class ClientDemo { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1",10000); //是本地的流,用来读取本地文件的. BufferedInputStream bis = new BufferedInputStream(new FileInputStream("socketmodule\\ClientDir\\1.jpg")); //写到服务器 --- 网络中的流 OutputStream os = socket.getOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(os); int b; while((b = bis.read())!=-1){ bos.write(b);//通过网络写到服务器中 } bos.flush(); //给服务器一个结束标记,告诉服务器文件已经传输完毕 socket.shutdownOutput(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line; while((line = br.readLine()) !=null){ System.out.println(line); } bis.close(); socket.close(); } } // 服务器 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); Socket accept = ss.accept(); //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("socketmodule\\ServerDir\\copy.jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); bos.close(); accept.close(); ss.close(); } }
3.5TCP程序服务器优化
优化方案一
- 需求
服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。 - 解决方案
使用循环 代码实现
// 服务器代码如下,客户端代码同上个案例,此处不再给出 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\copy.jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); bos.close(); accept.close(); } //ss.close(); } }
- 需求
优化方案二
- 需求
第二次上传文件的时候,会把第一次的文件给覆盖。 - 解决方案
UUID. randomUUID()方法生成随机的文件名 代码实现
// 服务器代码如下,客户端代码同上个案例,此处不再给出 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); bos.close(); accept.close(); } //ss.close(); } }
- 需求
优化方案三
- 需求
使用循环虽然可以让服务器处理多个客户端请求。但是还是无法同时跟多个客户端进行通信。 - 解决方案
开启多线程处理 代码实现
// 线程任务类 public class ThreadSocket implements Runnable { private Socket acceptSocket; public ThreadSocket(Socket accept) { this.acceptSocket = accept; } @Override public void run() { BufferedOutputStream bos = null; try { //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(acceptSocket.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(acceptSocket.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if(bos != null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (acceptSocket != null){ try { acceptSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 服务器代码 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); ThreadSocket ts = new ThreadSocket(accept); new Thread(ts).start(); } //ss.close(); } }
- 需求
优化方案四
- 需求
使用多线程虽然可以让服务器同时处理多个客户端请求。但是资源消耗太大。 - 解决方案
加入线程池 代码实现
// 服务器代码如下,线程任务类代码同上,此处不再给出 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); ThreadPoolExecutor pool = new ThreadPoolExecutor( 3,//核心线程数量 10, //线程池的总数量 60, //临时线程空闲时间 TimeUnit.SECONDS, //临时线程空闲时间的单位 new ArrayBlockingQueue<>(5),//阻塞队列 Executors.defaultThreadFactory(),//创建线程的方式 new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略 ); while (true) { Socket accept = ss.accept(); ThreadSocket ts = new ThreadSocket(accept); //new Thread(ts).start(); pool.submit(ts); } //ss.close(); } }
- 需求
4.NIO
4.1概述
- BIO
Blocking IO,阻塞型IO - NIO
No Blocking IO,非阻塞型IO - 阻塞IO的弊端
在等待的过程中,什么事也做不了 - 非阻塞IO的好处
不需要一直等待,当一切就绪了再去做
4.2NIO与BIO的区别
- 区别一
BIO是阻塞的,NIO是非阻塞的 - 区别二
BIO是面向流的,NIO是面向缓冲区的
BIO中数据传输是单向的,NIO中的缓冲区是双向的
4.3NIO三大模块
- 缓冲区
用来存储数据 - 通道
用来建立连接和传输数据 - 选择器
监视通道状态
4.4NIO创建缓冲区对象
方法介绍
| 方法名 | 说明 | | —- | —- | | static ByteBuffer allocate(长度) | 创建byte类型的缓冲区 | | static ByteBuffer wrap(byte[] array) | 创建一个有内容的byte类型缓冲区 |代码示例
public class CreateByteBufferDemo1 { public static void main(String[] args) { //method1(); //method2(); ByteBuffer wrap = ByteBuffer.wrap("aaa".getBytes()); for (int i = 0; i < 3; i++) { System.out.println(wrap.get()); } } private static void method2() { byte [] bytes = {97,98,99}; ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes); //缓冲区的长度3 //缓冲区里面的内容就是字节数组的内容. for (int i = 0; i < 3; i++) { System.out.println(byteBuffer2.get()); } System.out.println(byteBuffer2.get()); } private static void method1() { ByteBuffer byteBuffer1 = ByteBuffer.allocate(5); //get for (int i = 0; i < 5; i++) { System.out.println(byteBuffer1.get()); } System.out.println(byteBuffer1.get()); } }
4.5NIO缓冲区添加数据
- 方法介绍
- 代码示例
```java
public class ByteBufferDemo2 {
public static void main(String[] args) {
// int position() 当前要操作的索引
// int limit() 最多能操作到哪个索引
// int capacity() 缓冲区的总长度
ByteBuffer byteBuffer = ByteBuffer.allocate(10); System.out.println(byteBuffer.position());//0 System.out.println(byteBuffer.limit());//10 System.out.println(byteBuffer.capacity());//10
// put(byte b) 一次添加一个字节 // byteBuffer.put((byte) 97); // System.out.println(byteBuffer.position()); // System.out.println(byteBuffer.limit()); // System.out.println(byteBuffer.capacity());
// put(byte[] src) 一次添加一个字节数组 // byteBuffer.put(“aaa”.getBytes()); // System.out.println(byteBuffer.position());//3 // System.out.println(byteBuffer.limit());//10 // System.out.println(byteBuffer.capacity());//10
// position(int newPosition) 修改position // byteBuffer.position(1);
// limit(int newLimit) 修改limit // byteBuffer.limit(5); // System.out.println(byteBuffer.position()); // System.out.println(byteBuffer.limit()); // System.out.println(byteBuffer.capacity());
// int remaining() 还有多少能操作 // boolean hasRemaining() 是否还有能操作的
byteBuffer.put("0123456789".getBytes());
System.out.println(byteBuffer.remaining());
System.out.println(byteBuffer.hasRemaining());
}
}
<a name="wR53f"></a>
## 4.6NIO缓冲区获取数据
- 方法介绍
| 方法名 | 介绍 |
| --- | --- |
| flip() | 切换读写模式(写à读) |
| get() | 读一个字节 |
| get(byte[] dst) | 读多个字节 |
| get(int index) | 读指定索引的字节 |
| rewind() | 将position设置为0,可以重复读 |
| clear() | 数据读写完毕(读->写) |
| array() | 将缓冲区转换成字节数组返回 |
- 代码示例
```java
public class ByteBufferDemo3 {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put("abc".getBytes());
// flip() 切换读写模式(写读)
byteBuffer.flip();
// get() 读一个字节
// while(byteBuffer.limit() != byteBuffer.position()){
// System.out.println((char) byteBuffer.get());
// }
for (int i = 0; i < byteBuffer.limit(); i++) {
System.out.println((char) byteBuffer.get());
}
// get(byte[] dst) 读多个字节
// byte [] bytes = new byte[byteBuffer.limit()];
// byteBuffer.get(bytes);
// System.out.println(new String(bytes));
// get(int index) 读指定索引的字节
// System.out.println((char) byteBuffer.get(0));
// rewind() 将position设置为0,可以重复读
// byteBuffer.rewind();
// for (int i = 0; i < byteBuffer.limit(); i++) {
// System.out.println((char) byteBuffer.get());
// }
// clear() 数据读写完毕(读->写)
byteBuffer.clear();
byteBuffer.put("qqq".getBytes());
// array() 将缓冲区转换成字节数组返回
byte[] bytes = byteBuffer.array();
System.out.println(new String(bytes));
}
}
4.7小结
- 需求:我要把数据写到缓冲区中。
数据是从外面进入到缓冲区的,所以缓冲区在做读数据的操作。 - 需求:我要把数据从缓冲区中读出来。
数据是从缓冲区里面到外面的。所以缓冲区在做写数据的操作。 - capacity:容量(长度)
limit: 界限(最多能读/写到哪里)
posotion:位置(读/写哪个索引) - 获取缓冲区里面数据之前,需要调用flip方法
- 再次写数据之前,需要调用clear方法,
但是数据还未消失,等再次写入数据,被覆盖了才会消失。4.8 NIO通道客户端
- 客户端实现步骤
- 打开通道
- 指定IP和端口号
- 写出数据
- 释放资源
示例代码
public class NIOClient { public static void main(String[] args) throws IOException { //1.打开通道 SocketChannel socketChannel = SocketChannel.open(); //2.指定IP和端口号 socketChannel.connect(new InetSocketAddress("127.0.0.1",10000)); //3.写出数据 ByteBuffer byteBuffer = ByteBuffer.wrap("一点寒毛先制".getBytes()); socketChannel.write(byteBuffer); //4.释放资源 socketChannel.close(); } }
4.9 NIO通道服务端
- NIO通道
- 服务端通道
只负责建立建立,不负责传递数据 - 客户端通道
建立建立并将数据传递给服务端 - 缓冲区
客户端发送的数据都在缓冲区中 - 服务端通道内部创建出来的客户端通道
相当于客户端通道的延伸用来传递数据
- 服务端通道
- 服务端实现步骤
- 打开一个服务端通道
- 绑定对应的端口号
- 通道默认是阻塞的,需要设置为非阻塞
- 此时没有门卫大爷,所以需要经常看一下有没有连接发过来没?
- 如果有客户端来连接了,则在服务端通道内部,再创建一个客户端通道,相当于是客户端通道的延伸
- 获取客户端传递过来的数据,并把数据放在byteBuffer1这个缓冲区中
- 给客户端回写数据
- 释放资源
- 示例代码
public class NIOServer { public static void main(String[] args) throws IOException { // 1.打开一个服务端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 2.绑定对应的端口号 serverSocketChannel.bind(new InetSocketAddress(10000)); // 3.通道默认是阻塞的,需要设置为非阻塞 //如果传递true 表示通道设置为阻塞通道...默认值 //如果传递false 表示通道设置为非阻塞通道 serverSocketChannel.configureBlocking(false); // 4.此时没有门卫大爷,所以需要经常看一下有没有连接发过来没? while (true) { // 5.如果有客户端来连接了,则在服务端通道内部,再创建一个客户端通道,相当于是客户端通道的延伸 //此时已经设置了通道为非阻塞 //所以在调用方法的时候,如果有客户端来连接,那么会创建一个SocketChannel对象. //如果在调用方法的时候,没有客户端来连接,那么他会返回一个null SocketChannel socketChannel = serverSocketChannel.accept(); //System.out.println(socketChannel); if(socketChannel != null){ // 6.客户端将缓冲区通过通道传递给服务端,就到了这个延伸通道socketChannel里面 // 7.服务端创建一个空的缓冲区装数据并输出 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //获取传递过来的数据,并把他们放到byteBuffer缓冲区中. //返回值: //正数: 表示本次读到的有效字节个数. //0 : 表示本次没有读到有效字节. //-1 : 表示读到了末尾 int len = socketChannel.read(byteBuffer); System.out.println(new String(byteBuffer.array(),0,len)); //8.释放资源 socketChannel.close(); } } } }
4.10 NIO通道练习
客户端
- 实现步骤
- 打开通道
- 指定IP和端口号
- 写出数据
- 读取服务器写回的数据
- 释放资源
示例代码
public class Clinet { public static void main(String[] args) throws IOException { // 1.打开通道 SocketChannel socketChannel = SocketChannel.open(); // 2.指定IP和端口号 socketChannel.connect(new InetSocketAddress("127.0.0.1",10000)); // 3.写出数据 ByteBuffer byteBuffer1 = ByteBuffer.wrap("吃俺老孙一棒棒".getBytes()); socketChannel.write(byteBuffer1); // 手动写入结束标记 socketChannel.shutdownOutput(); System.out.println("数据已经写给服务器"); // 4.读取服务器写回的数据 ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024); int len; while((len = socketChannel.read(byteBuffer2)) != -1){ byteBuffer2.flip(); System.out.println(new String(byteBuffer2.array(),0,len)); byteBuffer2.clear(); } // 5.释放资源 socketChannel.close(); } }
- 实现步骤
服务端
- 实现步骤
- 打开一个服务端通道
- 绑定对应的端口号
- 通道默认是阻塞的,需要设置为非阻塞
- 此时没有门卫大爷,所以需要经常看一下有没有连接发过来没?
- 如果有客户端来连接了,则在服务端通道内部,再创建一个客户端通道,相当于是客户端通道的延伸
- 获取客户端传递过来的数据,并把数据放在byteBuffer1这个缓冲区中
- 给客户端回写数据
- 释放资源
示例代码
public class Sever { public static void main(String[] args) throws IOException { // 1,打开一个服务端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 2,绑定对应的端口号 serverSocketChannel.bind(new InetSocketAddress(10000)); // 3,通道默认是阻塞的,需要设置为非阻塞 serverSocketChannel.configureBlocking(false); // 4,此时没有门卫大爷,所以需要经常看一下有没有连接发过来没? while(true){ // 5,如果有客户端来连接了,则在服务端通道内部,再创建一个客户端通道,相当于是客户端通道的延伸 SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ System.out.println("此时有客户端来连接了"); // 6,获取客户端传递过来的数据,并把数据放在byteBuffer1这个缓冲区中 ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024); //socketChannel.read(byteBuffer1); int len; //针对于缓冲区来讲 //如果 从添加数据 ----> 获取数据 flip //如果 从获取数据 ----> 添加数据 clear while((len = socketChannel.read(byteBuffer1)) != -1){ byteBuffer1.flip(); System.out.println(new String(byteBuffer1.array(),0,len)); byteBuffer1.clear(); } System.out.println("接收数据完毕,准备开始往客户端回写数据"); // 7,给客户端回写数据 ByteBuffer byteBuffer2 = ByteBuffer.wrap("哎哟,真疼啊!!!".getBytes()); socketChannel.write(byteBuffer2); // 8,释放资源 socketChannel.close(); } } } }
- 实现步骤
4.11 NIO通道练习优化
- 存在问题
服务端内部获取的客户端通道在读取时,如果读取不到结束标记就会一直阻塞 - 解决方案
将服务端内部获取的客户端通道设置为非阻塞的 示例代码
// 客户端 public class Clinet { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1",10000)); ByteBuffer byteBuffer1 = ByteBuffer.wrap("吃俺老孙一棒棒".getBytes()); socketChannel.write(byteBuffer1); System.out.println("数据已经写给服务器"); ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024); int len; while((len = socketChannel.read(byteBuffer2)) != -1){ System.out.println("客户端接收回写数据"); byteBuffer2.flip(); System.out.println(new String(byteBuffer2.array(),0,len)); byteBuffer2.clear(); } socketChannel.close(); } } // 服务端 public class Sever { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(10000)); serverSocketChannel.configureBlocking(false); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannel != null){ System.out.println("此时有客户端来连接了"); // 将服务端内部获取的客户端通道设置为非阻塞的 socketChannel.configureBlocking(false); //获取客户端传递过来的数据,并把数据放在byteBuffer1这个缓冲区中 ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024); //socketChannel.read(byteBuffer1); int len; //针对于缓冲区来讲 //如果 从添加数据 ----> 获取数据 flip //如果 从获取数据 ----> 添加数据 clear while((len = socketChannel.read(byteBuffer1)) > 0){ System.out.println("服务端接收发送数据"); byteBuffer1.flip(); System.out.println(new String(byteBuffer1.array(),0,len)); byteBuffer1.clear(); } System.out.println("接收数据完毕,准备开始往客户端回写数据"); ByteBuffer byteBuffer2 = ByteBuffer.wrap("哎哟,真疼啊!!!".getBytes()); socketChannel.write(byteBuffer2); socketChannel.close(); } } } }
4.12 NIO选择器
- 概述
选择器可以监视通道的状态,多路复用
- 选择器对象
- Selector
选择器对象 - SelectionKey
绑定的key - SelectableChannel
能使用选择器的通道- SocketChannel
- ServerSocketChannel
- Selector
4.13 NIO选择器改写服务端
- 实现步骤
- 打开一个服务端通道(open)
- 绑定对应的端口号
- 通道默认是阻塞的,需要设置为非阻塞
- 打开一个选择器(门卫大爷)
- 将选择器绑定服务端通道,并监视服务端是否准备好
- 如果有客户端来连接了,大爷会遍历所有的服务端通道,谁准备好了,就让谁来连接
连接后,在服务端通道内部,再创建一个客户端延伸通道 - 如果客户端把数据传递过来了,大爷会遍历所有的延伸通道,谁准备好了,谁去接收数据
代码实现
// 客户端 public class Clinet { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1",10000)); ByteBuffer byteBuffer1 = ByteBuffer.wrap("吃俺老孙一棒棒".getBytes()); socketChannel.write(byteBuffer1); System.out.println("数据已经写给服务器"); ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024); int len; while((len = socketChannel.read(byteBuffer2)) != -1){ System.out.println("客户端接收回写数据"); byteBuffer2.flip(); System.out.println(new String(byteBuffer2.array(),0,len)); byteBuffer2.clear(); } socketChannel.close(); } } // 服务端 public class Server { public static void main(String[] args) throws IOException { //1.打开服务端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //2.让这个通道绑定一个端口 serverSocketChannel.bind(new InetSocketAddress(10000)); //3.设置通道为非阻塞 serverSocketChannel.configureBlocking(false); //4.打开一个选择器 //Selector --- 选择器 // SelectionKey --- 绑定通道后返回那个令牌 // SelectableChannel --- 可以使用选择器的通道 Selector selector = Selector.open(); //5.绑定选择器和服务端通道 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); while(true){ System.out.println("11"); //选择器会监视客户端通道的状态. //6.返回值就表示此时有多少个客户端来连接. int count = selector.select(); System.out.println("222"); if(count != 0){ System.out.println("有客户端来连接了"); //7.会遍历所有的服务端通道.看谁准备好了,谁准备好了,就让谁去连接. //获取所有服务端通道的令牌,并将它们都放到一个集合中,将集合返回. Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()){ //selectionKey 依次表示每一个服务端通道的令牌 SelectionKey selectionKey = iterator.next(); if(selectionKey.isAcceptable()){ //可以通过令牌来获取到了一个已经就绪的服务端通道 ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel(); //客户端的延伸通道 SocketChannel socketChannel = ssc.accept(); //将客户端延伸通道设置为非阻塞的 socketChannel.configureBlocking(false); socketChannel.register(selector,SelectionKey.OP_READ); //当客户端来连接的时候,所有的步骤已经全部执行完毕. }else if(selectionKey.isReadable()){ //当前通道已经做好了读取的准备(延伸通道) SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024); //socketChannel.read(byteBuffer1); int len; while((len = socketChannel.read(byteBuffer1)) > 0){ byteBuffer1.flip(); System.out.println(new String(byteBuffer1.array(),0,len)); byteBuffer1.clear(); } //给客户端的回写数据 socketChannel.write(ByteBuffer.wrap("哎哟喂好疼啊!!!".getBytes())); socketChannel.close(); } iterator.remove(); } } } } }
5.HTTP协议
5.1概述
超文本传输协议(关于超文本的概念JavaWeb在进行学习),是建立在TCP/IP协议基础上,是网络应用层的协议。
由请求和响应构成,是一个标准的客户端和服务器模型
5.2URL
- 概述
统一资源定位符,常见的如http://bbs.itheima.com/forum.php
完整的格式为 http://bbs.itheima.com:80/forum.php - 详解
5.3抓包工具的使用
- 使用步骤
- 在谷歌浏览器网页中按F12 或者网页空白处右键,点击检查,可以调出工具
- 点击network,进入到查看网络相关信息界面
- 这时在浏览器中发起请求,进行访问,工具中就会显示出请求和响应相关的信息
5.4请求信息
- 组成
- 请求行
- 请求头
- 请求空行
- 请求体
- 请求行
- 格式
- 请求方式
GET,POST,HEAD,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH
其中用的比较多的是GET和POST - URI
请求资源路径,统一资源标识符 - 协议版本
- HTTP1.0: 每次请求和响应都需要建立一个单独的连接
- HTTP1.1:支持长连接
- 格式
- 请求头
- 格式
- 请求头名称
- Host: 用来指定请求的服务端地址
- Connection: 取值为keep-alive表示需要持久连接
- User-Agent: 客户端的信息
- Accept: 指定客户端能够接收的内容类型
- Accept-Encoding: 指定浏览器可以支持的服务器返回内容压缩编码类型
- Accept-Language: 浏览器可接受的语言
- 格式
- 小结
5.5响应信息
- 组成
- 响应行
- 响应头
- 响应空行
- 响应体
- 响应行
- 格式
- 协议版本
- HTTP1.0: 每次请求和响应都需要建立一个单独的连接
- HTTP1.1: 支持长连接
- 响应状态码
- 1xx: 指示信息(表示请求已接收,继续处理)
- 2xx: 成功(表示请求已被成功接收、理解、接受)
- 3xx: 请求重定向(要完成请求必须进行更进一步的操作)
- 4xx: 客户端错误(请求有语法错误或请求无法实现)
- 5xx: 服务器端错误(服务器未能实现合法的请求)
- 状态信息
- 200 ok
- 404 Not Found
- 500 Internal Server Error
- 格式
- 响应头
- 响应头名称
- Content-Type: 告诉客户端实际返回内容的网络媒体类型(互联网媒体类型,也叫做MIME类型)
- 响应头值
- text/html ——> 文本类型
- image/png ——> png格式文件
- image/jpeg ——> jpg格式文件
- 响应头名称
- 小结
6.HTTP服务器
6.1需求
- 编写服务器端代码,实现可以解析浏览器的请求,给浏览器响应数据
6.2环境搭建
- 实现步骤
- 编写HttpServer类,实现可以接收浏览器发出的请求
- 其中获取连接的代码可以单独抽取到一个类中
代码实现
// 服务端代码 public class HttpServer { public static void main(String[] args) throws IOException { //1.打开服务端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //2.让这个通道绑定一个端口 serverSocketChannel.bind(new InetSocketAddress(10000)); //3.设置通道为非阻塞 serverSocketChannel.configureBlocking(false); //4.打开一个选择器 Selector selector = Selector.open(); //5.绑定选择器和服务端通道 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); while(true){ //6.选择器会监视通道的状态. int count = selector.select(); if(count != 0){ //7.会遍历所有的服务端通道.看谁准备好了,谁准备好了,就让谁去连接. //获取所有服务端通道的令牌,并将它们都放到一个集合中,将集合返回. Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()){ //selectionKey 依次表示每一个服务端通道的令牌 SelectionKey selectionKey = iterator.next(); if(selectionKey.isAcceptable()){ //获取连接 AcceptHandler acceptHandler = new AcceptHandler(); acceptHandler.connSocketChannel(selectionKey); }else if(selectionKey.isReadable()){ } //任务处理完毕以后,将SelectionKey从集合中移除 iterator.remove(); } } } } } // 将获取连接的代码抽取到这个类中 public class AcceptHandler { public SocketChannel connSocketChannel(SelectionKey selectionKey){ try { //获取到已经就绪的服务端通道 ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel(); SocketChannel socketChannel = ssc.accept(); //设置为非阻塞状态 socketChannel.configureBlocking(false); //把socketChannel注册到选择器上 socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); return socketChannel; } catch (IOException e) { e.printStackTrace(); } return null; } }
6.3获取请求信息并解析
- 实现步骤
- 将请求信息封装到HttpRequest类中
- 在类中定义方法,实现获取请求信息并解析
代码实现
/** * 用来封装请求数据的类 */ public class HttpRequest { private String method; //请求方式 private String requestURI; //请求的uri private String version; //http的协议版本 private HashMap<String,String> hm = new HashMap<>();//所有的请求头 //parse --- 获取请求数据 并解析 public void parse(SelectionKey selectionKey){ try { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); StringBuilder sb = new StringBuilder(); //创建一个缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len; //循环读取 while((len = socketChannel.read(byteBuffer)) > 0){ byteBuffer.flip(); sb.append(new String(byteBuffer.array(),0,len)); //System.out.println(new String(byteBuffer.array(),0,len)); byteBuffer.clear(); } //System.out.println(sb); parseHttpRequest(sb); } catch (IOException e) { e.printStackTrace(); } } //解析http请求协议中的数据 private void parseHttpRequest(StringBuilder sb) { //1.需要把StringBuilder先变成一个字符串 String httpRequestStr = sb.toString(); //2.获取每一行数据 String[] split = httpRequestStr.split("\r\n"); //3.获取请求行 String httpRequestLine = split[0];//GET / HTTP/1.1 //4.按照空格进行切割,得到请求行中的三部分 String[] httpRequestInfo = httpRequestLine.split(" "); this.method = httpRequestInfo[0]; this.requestURI = httpRequestInfo[1]; this.version = httpRequestInfo[2]; //5.操作每一个请求头 for (int i = 1; i < split.length; i++) { String httpRequestHeaderInfo = split[i];//Host: 127.0.0.1:10000 String[] httpRequestHeaderInfoArr = httpRequestHeaderInfo.split(": "); hm.put(httpRequestHeaderInfoArr[0],httpRequestHeaderInfoArr[1]); } } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getRequestURI() { return requestURI; } public void setRequestURI(String requestURI) { this.requestURI = requestURI; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public HashMap<String, String> getHm() { return hm; } public void setHm(HashMap<String, String> hm) { this.hm = hm; } @Override public String toString() { return "HttpRequest{" + "method='" + method + '\'' + ", requestURI='" + requestURI + '\'' + ", version='" + version + '\'' + ", hm=" + hm + '}'; } }
6.4给浏览器响应数据
- 实现步骤
- 将响应信息封装HttpResponse类中
- 定义方法,封装响应信息,给浏览器响应数据
代码实现
public class HttpResponse { private String version; //协议版本 private String status; //响应状态码 private String desc; //状态码的描述信息 //响应头数据 private HashMap<String, String> hm = new HashMap<>(); private HttpRequest httpRequest; //我们后面要根据请求的数据,来进行一些判断 //给浏览器响应数据的方法 public void sendStaticResource(SelectionKey selectionKey) { //1.给响应行赋值 this.version = "HTTP/1.1"; this.status = "200"; this.desc = "ok"; //2.将响应行拼接成一个单独的字符串 // HTTP/1.1 200 ok String responseLine = this.version + " " + this.status + " " + this.desc + "\r\n"; //3.给响应头赋值 hm.put("Content-Type", "text/html;charset=UTF-8"); //4.将所有的响应头拼接成一个单独的字符串 StringBuilder sb = new StringBuilder(); Set<Map.Entry<String, String>> entries = hm.entrySet(); for (Map.Entry<String, String> entry : entries) { sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); } //5.响应空行 String emptyLine = "\r\n"; //6.响应行,响应头,响应空行拼接成一个大字符串 String responseLineStr = responseLine + sb.toString() + emptyLine; try { //7.将上面三个写给浏览器 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer1 = ByteBuffer.wrap(responseLineStr.getBytes()); socketChannel.write(byteBuffer1); //8.单独操作响应体 //因为在以后响应体不一定是一个字符串 //有可能是一个文件,所以单独操作 String s = "哎哟,妈呀,终于写完了."; ByteBuffer byteBuffer2 = ByteBuffer.wrap(s.getBytes()); socketChannel.write(byteBuffer2); //9.释放资源 socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public HashMap<String, String> getHm() { return hm; } public void setHm(HashMap<String, String> hm) { this.hm = hm; } public HttpRequest getHttpRequest() { return httpRequest; } public void setHttpRequest(HttpRequest httpRequest) { this.httpRequest = httpRequest; } @Override public String toString() { return "HttpResponse{" + "version='" + version + '\'' + ", status='" + status + '\'' + ", desc='" + desc + '\'' + ", hm=" + hm + ", httpRequest=" + httpRequest + '}'; } }
6.5代码优化
- 实现步骤
- 根据请求资源路径不同,响应不同的数据
- 服务端健壮性处理
- 访问不存在的资源处理
代码实现
/** * 接收连接的任务处理类 */ public class AcceptHandler { public SocketChannel connSocketChannel(SelectionKey selectionKey){ try { //获取到已经就绪的服务端通道 ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel(); SocketChannel socketChannel = ssc.accept(); //设置为非阻塞状态 socketChannel.configureBlocking(false); //把socketChannel注册到选择器上 socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); return socketChannel; } catch (IOException e) { e.printStackTrace(); } return null; } } /** * 接收客户端请求的类 */ public class HttpServer { public static void main(String[] args) throws IOException { //1.打开服务端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //2.让这个通道绑定一个端口 serverSocketChannel.bind(new InetSocketAddress(10000)); //3.设置通道为非阻塞 serverSocketChannel.configureBlocking(false); //4.打开一个选择器 Selector selector = Selector.open(); //5.绑定选择器和服务端通道 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); while(true){ //6.选择器会监视通道的状态. int count = selector.select(); if(count != 0){ //7.会遍历所有的服务端通道.看谁准备好了,谁准备好了,就让谁去连接. //获取所有服务端通道的令牌,并将它们都放到一个集合中,将集合返回. Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()){ //selectionKey 依次表示每一个服务端通道的令牌 SelectionKey selectionKey = iterator.next(); if(selectionKey.isAcceptable()){ //获取连接 AcceptHandler acceptHandler = new AcceptHandler(); acceptHandler.connSocketChannel(selectionKey); }else if(selectionKey.isReadable()){ //读取数据 HttpRequest httpRequest = new HttpRequest(); httpRequest.parse(selectionKey); System.out.println("http请求的数据为 ---->" + httpRequest); if(httpRequest.getRequestURI() == null || "".equals(httpRequest.getRequestURI())){ selectionKey.channel(); continue; } System.out.println("...数据解析完毕,准备响应数据...."); //响应数据 HttpResponse httpResponse = new HttpResponse(); httpResponse.setHttpRequest(httpRequest); httpResponse.sendStaticResource(selectionKey); } //任务处理完毕以后,将SelectionKey从集合中移除 iterator.remove(); } } } } } /** * 用来封装请求数据的类 */ public class HttpRequest { private String method; //请求方式 private String requestURI; //请求的uri private String version; //http的协议版本 private HashMap<String,String> hm = new HashMap<>();//所有的请求头 //parse --- 获取请求数据 并解析 public void parse(SelectionKey selectionKey){ try { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); StringBuilder sb = new StringBuilder(); //创建一个缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len; //循环读取 while((len = socketChannel.read(byteBuffer)) > 0){ byteBuffer.flip(); sb.append(new String(byteBuffer.array(),0,len)); //System.out.println(new String(byteBuffer.array(),0,len)); byteBuffer.clear(); } //System.out.println(sb); parseHttpRequest(sb); } catch (IOException e) { e.printStackTrace(); } } //解析http请求协议中的数据 private void parseHttpRequest(StringBuilder sb) { //1.需要把StringBuilder先变成一个字符串 String httpRequestStr = sb.toString(); if(!(httpRequestStr == null || "".equals(httpRequestStr))){ //2.获取每一行数据 String[] split = httpRequestStr.split("\r\n"); //3.获取请求行 String httpRequestLine = split[0];//GET / HTTP/1.1 //4.按照空格进行切割,得到请求行中的三部分 String[] httpRequestInfo = httpRequestLine.split(" "); this.method = httpRequestInfo[0]; this.requestURI = httpRequestInfo[1]; this.version = httpRequestInfo[2]; //5.操作每一个请求头 for (int i = 1; i < split.length; i++) { String httpRequestHeaderInfo = split[i];//Host: 127.0.0.1:10000 String[] httpRequestHeaderInfoArr = httpRequestHeaderInfo.split(": "); hm.put(httpRequestHeaderInfoArr[0],httpRequestHeaderInfoArr[1]); } } } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getRequestURI() { return requestURI; } public void setRequestURI(String requestURI) { this.requestURI = requestURI; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public HashMap<String, String> getHm() { return hm; } public void setHm(HashMap<String, String> hm) { this.hm = hm; } @Override public String toString() { return "HttpRequest{" + "method='" + method + '\'' + ", requestURI='" + requestURI + '\'' + ", version='" + version + '\'' + ", hm=" + hm + '}'; } } /** * 用来封装响应数据的类 */ public class HttpResponse { private String version; //协议版本 private String status; //响应状态码 private String desc; //状态码的描述信息 //响应头数据 private HashMap<String, String> hm = new HashMap<>(); private HttpRequest httpRequest; //我们后面要根据请求的数据,来进行一些判断 //给浏览器响应数据的方法 public void sendStaticResource(SelectionKey selectionKey) { //1.给响应行赋值 this.version = "HTTP/1.1"; this.status = "200"; this.desc = "ok"; //3.给响应头赋值 //先获取浏览器请求的URI String requestURI = this.getHttpRequest().getRequestURI(); if(requestURI != null){ File file = new File(WEB_APP_PATH + requestURI); //判断这个路径是否存在 if(!file.exists()){ this.status = "404"; this.desc = "NOT FOUNG"; } if("200".equals(this.status)){ if("/".equals(requestURI)){ hm.put("Content-Type", "text/html;charset=UTF-8"); }else if("/favicon.ico".equals(requestURI)){ hm.put("Content-Type", "image/x-icon"); }else if("/a.txt".equals(requestURI)){ hm.put("Content-Type", "text/html;charset=UTF-8"); }else if("/1.jpg".equals(requestURI)){ hm.put("Content-Type", "image/jpeg"); }else if("/1.png".equals(requestURI)){ hm.put("Content-Type", "image/png"); } }else{ hm.put("Content-Type", "text/html;charset=UTF-8"); } } //2.将响应行拼接成一个单独的字符串 // HTTP/1.1 200 ok String responseLine = this.version + " " + this.status + " " + this.desc + "\r\n"; //4.将所有的响应头拼接成一个单独的字符串 StringBuilder sb = new StringBuilder(); Set<Map.Entry<String, String>> entries = hm.entrySet(); for (Map.Entry<String, String> entry : entries) { sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); } //5.响应空行 String emptyLine = "\r\n"; //6.响应行,响应头,响应空行拼接成一个大字符串 String responseLineStr = responseLine + sb.toString() + emptyLine; try { //7.将上面三个写给浏览器 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer1 = ByteBuffer.wrap(responseLineStr.getBytes()); socketChannel.write(byteBuffer1); //8.单独操作响应体 //因为在以后响应体不一定是一个字符串 //有可能是一个文件,所以单独操作 // String s = "哎哟,妈呀,终于写完了."; byte [] bytes = getContent(); ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes); socketChannel.write(byteBuffer2); //9.释放资源 socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } public static final String WEB_APP_PATH = "mynio\\webapp"; private byte[] getContent() { try { //1.获取浏览器请求的URI String requestURI = this.getHttpRequest().getRequestURI(); if(requestURI != null){ if("200".equals(this.status)){ //2.判断一下请求的URI,根据不同的URI来响应不同的东西 if("/".equals(requestURI)){ String s = "哎哟,妈呀,终于写完了."; return s.getBytes(); }else/* if("/favicon.ico".equals(requestURI))*/{ //获取一个ico文件 FileInputStream fis = new FileInputStream(WEB_APP_PATH + requestURI); //把ico文件变成一个字节数组返回 return IOUtils.toByteArray(fis); } }else{ return "访问的资源不存在".getBytes(); } } } catch (IOException e) { e.printStackTrace(); } return new byte[0]; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public HashMap<String, String> getHm() { return hm; } public void setHm(HashMap<String, String> hm) { this.hm = hm; } public HttpRequest getHttpRequest() { return httpRequest; } public void setHttpRequest(HttpRequest httpRequest) { this.httpRequest = httpRequest; } @Override public String toString() { return "HttpResponse{" + "version='" + version + '\'' + ", status='" + status + '\'' + ", desc='" + desc + '\'' + ", hm=" + hm + ", httpRequest=" + httpRequest + '}'; } }