初始网络编程
- 常见浏览器的架构
有些比较大型的软件这2种架构都会兼顾
- 2种架构的优缺点比较
B/S架构,以网页游戏为例
- 画面烂
C/S架构
在c/s架构中,安装包中已经有了软件运行的资源,如图片。在运行的时候不需要传输
网络编程三要素
一个端口号只能被一个软件绑定使用
三要素(IP)
IPV4
地址表示每个字节不能为负数,取值在0-255
IPV4使用32位表示ip地址,最多只能有42亿多个,不够用。由此引出IPV6
IPV6
当中间有多个0,直接写2个冒号省略中间的0
IPV4的一些小细节
- 以网吧为例子
大量电脑共享一个公网IP,然后为电脑分配局域网IP
如上图所示,如果我们想往自己的电脑上去发送信息,如果我们的ip地址写的是上面显示的ip地址。我们电脑的局域网ip地址,是由公有网进行分配,在不同的地址是不同的,每一个路由器给你分配的Ip是有肯能是不同的。我们的请求会先发送给路由器,然后由路由器发送给我们的电脑。
而当我们的电脑ip地址写的是127.0.0.1,当我们的信息经过网卡,将直接发送给我们的电脑
常见cmd命令
ping+Ip地址,可以检查该电脑和ip地址的电脑网络是否畅通
ping +www.baidu.com 检查和外网是否畅通
InetAddress的使用(用来表示ip的类)
在底层其实会判断本地系统使用的是ipv4还是ipv6,如果是ipv4将会返回ipv4的对象,如果使用ipv6将会返回ipv6的对象
-该类没有对外提供构造方法,只能用静态方法返回对象
验证返回Ip地址对象的静态方法
package com.cook.inet;
import java.net.InetAddress;
import java.net.UnknownHostException;
/*
演示:创建ip对象的静态方法
static InetAddress getByName(String host)确定主机名称的ip地址或计算机名(计算机名在我的电脑属性中可以查看)
static getHostName() 获取此IP地址的主机名
String getHostAdderss()返回文本显示中的IP地址字符串
*/
public class MyInetAddress1 {
public static void main(String[] args) throws UnknownHostException {
//获取InetAddress对象(方法底层或根据系统使用的协议返回IPv4后IPv6的地址)
//我们的ip地址其实表示的计算机,获取Ip对象其实就是获取计算机的对象
final InetAddress adderss = InetAddress.getByName("192.3.4.5");//获取IP对象
System.out.println(adderss);///197.23.23.4
final InetAddress adderss1 = InetAddress.getByName("LAPTOP-7KNA7AM8");//参数也可以是主机名
System.out.println(adderss1);//LAPTOP-7KNA7AM8/192.168.43.126
final String hostAddress = adderss.getHostAddress();//获取本机的Ip地址
System.out.println(hostAddress);//197.23.23.4
final String hostName = adderss.getHostName();//获取本机的主机名
System.out.println(hostName);//192.3.4.5(此时我们的这个ip地址在局域网中没有,将会返回ip地址)
}
}
细节:如果getHostName方法返回的是主机名称,而如果我们的电脑因为网络原因或者是局域网中没有我们这台电脑,这个方法将会返回ip地址
端口号
我们自己只能使用1024以上的端口号
端口理解为电脑往外发送数据或接收数据的出口或入口,我们的软件在启动是必须和一个端口号进行绑定,才能对外发送或接收数据
上图中电脑A中的微信网电脑B中的微信发送信息,电脑A通过端口65533发送出去,然后电脑B中的微信也用65533端口进行接收信息(将微信的端口号设置为65533)
- 总结
协议(数据传输的规则)
在这种模型中,程序员在应用层发送数据,会先向下传输到物理层转换成二进制,然后传输给别人,然后向上传输该别人的应用层显示
这种模型过于复杂和理想化并没有被采用
面向无连接:不管2台电脑的网络是否链接成功,就进行数据发送,不管是否对方电脑是否接收到数据(所以会导致数据不安全)
UTP的应用场景:如语音通话 网络会议 在线视频,丢失一点数据不会产生影响
Tcp应用场景(对数据要求高,不能丢失数据的):如:下载软件 文字聊天 发送邮件
UTP通信程序
UTP协议(发送数据)
以现实中打包快递的过程来理解发送数据的过程
package com.cook.inet;
import java.io.IOException;
import java.net.*;
/*
演示使用UDP协议发送数据
1.找到发送者 2.打包数据 3.发送数据
*/
public class SendMassageDemo1 {
public static void main(String[] args) throws IOException {
//创建DatagramSocker(快递公司)
//细节:
//这个类的构造方法可以绑定端口
//无参构造:在所有可用的端口中随机选择一个使用
//有参构造:指定端口进行绑定
DatagramSocket ds = new DatagramSocket();
//打包数据
//数据通过字节数组发送
String str = "你好";
final byte[] bytes = str.getBytes();//将数据转换成字节数组
//获取ip地址
final InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;//获取端口号
//其构造方法由:1.发送的数据(数据源+数据大小)2.接收者的信息(IP 和端口号)
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
//3.发送数据
ds.send(dp);//将数据放到发送公司中发送
//4.释放资源
ds.close();//只用发送数据的类释放资源即可
}
}
UDP协议(接收数据)
关于发到和发出端口号的界定:对于从哪个端口号中发出的随便,只需要保证我们要发到的端口号会接收的到的端口号相同即可
package com.cook.inet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
//演示UDP协议接收数据
public class ReceiveMessage1 {
public static void main(String[] args) throws IOException {
//1.创建接收端对象(找快递公司)
//细节:
//在接收的时候,一定要绑定端口
//要求绑定的端口和发送端指定要发送的端口号一致
DatagramSocket ds = new DatagramSocket(10086);//参数为接收端端口号
//2.接收数据包(接收箱子)(需要另外创建一个数据包用于接收)
//需要创建一个字节数据来接收
byte[] bytes = new byte[1024];//长度为1024个字节
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
ds.receive(dp);
//解析数据包(看看都可用接收到那些数据)
final InetAddress address = dp.getAddress();//发送者IP地址
final int length = dp.getLength();//字节数组长度(数据长度)
final int port = dp.getPort();//发送者端口号
final byte[] datas = dp.getData();//内容
System.out.println("接收到的数据为"+new String(datas,0,length));//将数据转换为字符串输出
System.out.println("该数据是从"+address+"这台电脑中"+port+"这个端口中发出的");
//4.释放资源
ds.close();//(只用释放装数据的箱子)
}
}
- 注意:需要先运行接收的程序然后运行发送的程序
结合前面发送端的数据, 可以有以下运行结果:
UDP练习(聊天室)
- 发送端
package com.cook.inet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class SendMassage2 {
public static void main(String[] args) throws IOException {
//1.创建DatagramStocket(快递公司)
DatagramSocket ds = new DatagramSocket();//不指定端口将会随机选择端口发出
//2.打包数据
//键盘录入数据
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("请输入要发送的数据:");
String massage = sc.nextLine();
if(massage.equals("886")){//输入的数据是886退出
break;
}else {
final byte[] bytes = massage.getBytes();//将字符串转换成字节数组
int port = 10086;//指定要发送到的端口
InetAddress address = InetAddress.getByName("127.0.0.1");//该类的对象需要静态方法获取
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
//发送数据
ds.send(dp);
//释放资源
}
}
ds.close();
}
}
- 接收端
package com.cook.inet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class ReceiveMessage2 {
public static void main(String[] args) throws IOException {
/*
按照下面要求实现程序:
UDP发送数据,数据来自键盘录入,直到发送的数据是886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送数据,故采用死循环接收
*/
//1.创建DatagramSocket(快递公司)
DatagramSocket ds = new DatagramSocket(10086);//并手动绑定接收的端口
//2.创建一个新的包存放接收的数据
byte[] bytes = new byte[1024];//定义一个1024字节的数组存放接收数据
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
while (true) {
ds.receive(dp);
//3.解析包裹
final InetAddress address = dp.getAddress();//发送端IP
final int length = dp.getLength();//数据长度
final byte[] data = dp.getData();//数据
final int port = dp.getPort();//端口号(发送端的端口
// )
System.out.println("接收到的数据为"+new String(bytes,0,length));
System.out.println("该数据来自地址为"+address+"端口为"+port);
}
}
}
可以修改IDEA的设置让其可以同时运行多个类,以形成一个聊天室,多个发送端发送数据,一个接收端接收数据
UTP的3种通信方式(单播 组播 广播)
一对一发送数据
给一组电脑发送数据
给局域网中所有的电脑发送数据
-
组播地址和IP地址的区别
之前的IP只能表示一台电脑这里随便一个组播地址可以表示多台电脑,如果在发送数据的时候发送到一个组播地址上,所有以该地址为组播地址的电脑都可以接收到该信息
这3种方式的代码实现 -
组播
-
发送端
package com.cook.inet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class SentMassage3 {
//验证组播发送数据
public static void main(String[] args) throws IOException {
//创建MulticastSocket对象(快递公司)
MulticastSocket ms = new MulticastSocket();//随机选择端口发出
//打包数据
String massage = "你好";//数据
final byte[] bytes = massage.getBytes();//将数据变成字节数组
int port = 10000;//接收的端口
InetAddress.getByName("224.0.0.1");//此地址为组播地址
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);//将所有数据打包
//发送数据
//释放资源
}
}
- 接收端
package com.cook.inet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class ReceiveMessage3 {
//验证组播接收数据
public static void main(String[] args) throws IOException {
//1.创建MulticastSocket对象(快递公司)
MulticastSocket ms = new MulticastSocket(10000);
//2.将本机添加到224.0.0.1这一组中
InetAddress address= InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
//2.准备包裹装接收到的数据
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);//将字节数组所有的长度用来接收数据
//3.接收数据
ms.receive(dp);
//3.解析数据
final InetAddress address1 = dp.getAddress();//获得地址
final byte[] data = dp.getData();//数据
final int length = dp.getLength();//数据长度
final int port = dp.getPort();//端口
System.out.println("接收到的数据为:"+new String(data,0,length));
System.out.println("数据来自:"+"IP为:"+address1+"端口为"+port);
}
}
注意:组播的还没有实验(多个接收端,只有一组接收到了数据)
- 广播
广播的代码和单播的代码基本相同,只需要在发送时把接收端的IP地址设置成广播地址255.255.255.255,就可以实现对局域网中所有的计算机发送数据
TCP的通信
注意在发送数据和接收数据的时候,发送端和接收端流的方向不一样
- 代码演示
此时的输入输出流直接以自身为参照即可
- 客户端
package com.cook.Tct;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
//模拟Tcp发送数据(客户端)
public class Client {
public static void main(String[] args) throws IOException {
//1.创建Socket对象
//该对象在创建的时候会同时连接服务器
//连接不上将会报错
Socket socket = new Socket("127.0.0.1",10005);//指定服务端的ip和端口
//2.连接成功后从连接通道中获取输出流写入数据
final OutputStream os = socket.getOutputStream();
os.write("天才在左疯子在右!".getBytes());//写入数据
//3.释放资源
os.close();
socket.close();
}
}
- 服务端
package com.cook.Tct;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
//模拟Tcp接收数据(服务器端)
public class Server {
public static void main(String[] args) throws IOException {
//1.创建SocketSever对象并绑定端口
ServerSocket ss = new ServerSocket(10005);
//2.监听客户端的连接(如果有连接将返回客户端的连接对象)
final Socket socket = ss.accept();//如果一直没有客户端进行连接,程序将停止在这里
//3.从连接通道中获取输入流读取数据
final InputStream is = socket.getInputStream();
int b;
//进行循环读取
while ((b = is.read())!= -1){
System.out.print((char)b );
}
//4.释放资源
is.close();
ss.close();
}
}
如上图所示,这种情况下传递中文将会出现乱码的问题
Tcp协议中文乱码的问题
1.分析
在字符集编码中中文和英文的编码方式不一样,没有指定编码将按照idea的默认的UTF-8编码,每个汉字使用3个字节表示,当我们 将汉字写入到服务器,4个汉字将会变成12个字节,而我们在服务器端进行读出时,是一个字节一个字节进行解码的,这势必造成了中文乱码的问题,这时候我们的解决方案就是改变输入流
我们可以将字节流包装成字符流,这样当遇到中文时将一次读取3个字节,将不会出现乱码问题
package com.cook.Tct;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
//模拟Tcp接收数据(服务器端)
public class Server {
public static void main(String[] args) throws IOException {
//1.创建SocketSever对象并绑定端口
ServerSocket ss = new ServerSocket(10005);
//2.监听客户端的连接(如果有连接将返回客户端的连接对象)
final Socket socket = ss.accept();//如果一直没有客户端进行连接,程序将停止在这里
//3.从连接通道中获取输入流读取数据
final InputStream is = socket.getInputStream();
InputStreamReader sr = new InputStreamReader(is);//将字节流包装成字符流
int b;
//进行循环读取
while ((b = sr.read())!= -1){
System.out.print((char)b );
}
//4.释放资源
is.close();
ss.close();
}
}
Tcp协议(代码细节)
1.在运行代码的时候必须要先运行服务端
2。在客户端的底层和采用三次握手协议来包装连接成功
3.我们的输入输出流是在连接通道里面的,我们只需要把Socket关闭(即关闭连接通道),流自然就关闭了(所以流可以关也可以不关)
在写入数据的时候,我们可以理解成将数据写入到了连接通道中,我们的服务器端需要从连接通道中将数据读取出来
关闭通道资源的4次挥手协议
在程序运行过程中,服务器端先运行等待连接,我们的客户端写入数据,客户端边写入服务器端边处理。这时如果我们的客户端的socet.close关闭通道资源,通道关闭了,我们的数据就不能都保证被服务端处理了。这时就在关闭资源时存在一个四次挥手协议,保证我们的数据都被处理了
三次握手与四次挥手
三次握手
四次挥手
和三次握手相比,四次挥手需要保证服务端把连接通道里面的数据处理完毕了
综合练习
- 客户端
package com.cook.Tct;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//练习1
//客户端:实现多次发送数据
public class Client1 {
public static void main(String[] args) throws IOException {
//1.创建Socket对象并连接服务器端
Socket socket = new Socket("127.0.0.1",10006);
//2.连接成功后获取输出流对象并写入数据
final OutputStream os = socket.getOutputStream();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入你要发送的数据:");
final String s = sc.next();
if(s.equals("886")){
break;
}
os.write(s.getBytes());//写入数据
}
//3.关闭资源
socket.close();
}
}
- 服务器端
package com.cook.Tct;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//练习1
public class Server1 {
public static void main(String[] args) throws IOException {
//实现多次接收数据并打印
//1.获取SocketSever对象并绑定端口
ServerSocket ss = new ServerSocket(10006);
//2.监听客户端的连接,成功则返回服务端对象
final Socket socket = ss.accept();
//3.获取输入流对象
final InputStream is = socket.getInputStream();
InputStreamReader ir = new InputStreamReader(is);//经字节流包装成字符流包装对中文的读出
int b;
while ((b = ir.read()) != -1){
System.out.print((char) b);
}
//4.关闭资源
ss.close();
is.close();
}
}
感悟:之前在想是不是在服务器端的数据读出的操作中是不是也要加一个while循环以保证实现一次数据读出的时候程序不会停止以读出多轮数据。发现完全不用,我们的资源关闭存在4次挥手操作以保证我们的数据完全处理完毕,所以只要当我们的客户端还有数据等待写入(客户端没有关闭)的时候,我们的服务器端就不会关闭
-
数据方向
当我们的服务端读取完数据会继续回到循环中继续读取数据直到读到文件结尾位置,程序将会卡死在循环中 -
客户端
package com.cook.Tct;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client2 {
public static void main(String[] args) throws IOException {
//客户端:发送一条数据,接收服务端反馈的消息并打印
//1.创建Socket对象并连接服务器
Socket socket = new Socket("127.0.0.1", 10007);
//2.获取输出流对象并发送消息
final OutputStream os = socket.getOutputStream();
Scanner sc = new Scanner(System.in);//使用键盘接收
System.out.println("请输入你想发送的数据:");
final String str = sc.next();
os.write(str.getBytes());
//写一个结束标记
socket.shutdownOutput();//相当于将输出流结束(服务器端将会收到这个标记)
//接收服务端的回写数据
final InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);//将输入流包装成字符流
int b;
while ((b = isr.read()) != -1){
System.out.println((char) b);
}
//3.关闭资源
socket.close();
}
}
- 服务器端
package com.cook.Tct;
import javax.swing.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server2 {
public static void main(String[] args) throws IOException {
//服务端:接收数据并打印,再给客户端返回消息
//1.创建SeverSocket对象并绑定端口
ServerSocket ss = new ServerSocket(10007);
//2.监听客户端的连接,成功则返回客户端对象
final Socket socket = ss.accept();
//3.成功之后获取输入流对象并打印数据
final InputStream is = socket.getInputStream();
InputStreamReader ir = new InputStreamReader(is);//将输入流包装成字符流
int b;
/*
细节:
read方法会从连接通道中读取数据
但是需要一个结束标记此处的循环才会停止
否则,程序就会一直停止在read这里,等待读取下面的数据
*/
while ((b = ir.read())!=-1){
System.out.print((char) b);
}
//4.回写数据
final OutputStream os = socket.getOutputStream();
String hh ="hhhhh";
os.write(hh.getBytes());
//关闭资源(2个通道资源都要关闭)
socket.close();
ss.close();
}
}
将本地IO流和网络IO流练习起来了
- 客户端
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.spec.RSAOtherPrimeInfo;
//练习3
public class Client3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
/*
思路:从本地文件读取数据到内存,然后由客户端发送的服务器
*/
//1.创建Socket对象并连接服务器
Socket socket = new Socket("127.0.0.1",10009);
//2.将文件读取到内存中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\ideaprojects\\myThread1\\Server.txt"));
//3.通过Socket对象获取输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());//将输出流包装成缓冲流
//4.同时进行从文件中读取数据和将数据写入到服务端的操作
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);//将数组中的有效数据都进行写入(不包含残余数据)
}
//5.往服务器写出结束标记
socket.shutdownOutput();
//6.接收服务器回写数据
//获取连接通道的字节流,将其转成字符流然后由缓冲流进行包裹
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
final String line = br.readLine();
System.out.println(line);
//7. 释放资源
socket.close();
}
}
- 服务器端
package com.cook.Tct;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//练习3
public class Server3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
//1.创建Socket对象并绑定端口
ServerSocket socket = new ServerSocket(10009);
//2.等待客户端的连接(如果有客户端连接将返回客户端的连接对象)
final Socket socket1 = socket.accept();
//3.从连接通道中读取数据+将读取的数据保存到本地文件中
BufferedInputStream bis = new BufferedInputStream(socket1.getInputStream());//获取输入流并进行包裹
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\ideaprojects\\myThread1\\Server.txt"));
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket1.getOutputStream()));
bw.write("上传成功");
bw.newLine();//写入换行
bw.flush();
//5.释放资源
socket1.close();
socket.close();
}
}
备注:此代码在将数据传递到文件没有成功,特此备注
接上面改正,将传递的文件改成jpg形式就成功了,经过测试可以成功传递
- 客户端
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.spec.RSAOtherPrimeInfo;
//练习3
public class Client3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
/*
思路:从本地文件读取数据到内存,然后由客户端发送的服务器
*/
//1.创建Socket对象并连接服务器
Socket socket = new Socket("127.0.0.1",10009);
//2.将文件读取到内存中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\dest\\clent.jpg"));
//3.通过Socket对象获取输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());//将输出流包装成缓冲流
//4.同时进行从文件中读取数据和将数据写入到服务端的操作
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);//将数组中的有效数据都进行写入(不包含残余数据)
}
//5.往服务器写出结束标记
socket.shutdownOutput();
//6.接收服务器回写数据
//获取连接通道的字节流,将其转成字符流然后由缓冲流进行包裹
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
final String line = br.readLine();
System.out.println(line);
//7. 释放资源
socket.close();
}
}
- 服务端
package com.cook.Tct;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//练习3
public class Server3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
//1.创建Socket对象并绑定端口
ServerSocket socket = new ServerSocket(10009);
//2.等待客户端的连接(如果有客户端连接将返回客户端的连接对象)
final Socket socket1 = socket.accept();
//3.从连接通道中读取数据+将读取的数据保存到本地文件中
BufferedInputStream bis = new BufferedInputStream(socket1.getInputStream());//获取输入流并进行包裹
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("C:\\dest1\\a.jpg"));//这里需要完整文件名
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket1.getOutputStream()));
bw.write("上传成功");
bw.newLine();//写入换行
bw.flush();
//5.释放资源
socket1.close();
socket.close();
}
}
还是上面的问题:为什么换成txt文件就传递不了了
解决上一题中文件重名的问题
在上面的代码中我们的服务器端上传生成的文件都叫a.txt,这会造成传递的文件把上一个文件给覆盖了
java中提供了UUID类,可以生成随机的字符串,让我们的文件名不一样,获取UUID的对象,可以使用静态方法randomUUID获取UUID的对象
在这里我们只需要修改服务端的代码即可,这样就可以实现,我们多次运行程序可以传递生成多个文件名不同但内容相同的jpg文件
基本思路:可以在服务端用循环让程序不停止运行,让其可以持续接收传输的文件
仅仅用循环改写的弊端:这是一个单线程程序:只有当一个文件已经传输完成时,才能开始第二个文件的传输。当我们的文件很大的时候,还在while循环中传输,在这时候我们的服务端是无法和其他的客户端相连的。这将会导致效率问题。这种情况下可以使用多线程来改进
改进方案:循环+多线程
- 客户端不变
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.spec.RSAOtherPrimeInfo;
//练习3
public class Client3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
/*
思路:从本地文件读取数据到内存,然后由客户端发送的服务器
*/
//1.创建Socket对象并连接服务器
Socket socket = new Socket("127.0.0.1",10009);
//2.将文件读取到内存中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\dest\\clent.jpg"));
//3.通过Socket对象获取输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());//将输出流包装成缓冲流
//4.同时进行从文件中读取数据和将数据写入到服务端的操作
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);//将数组中的有效数据都进行写入(不包含残余数据)
}
//5.往服务器写出结束标记
socket.shutdownOutput();
//6.接收服务器回写数据
//获取连接通道的字节流,将其转成字符流然后由缓冲流进行包裹
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
final String line = br.readLine();
System.out.println(line);
//7. 释放资源
socket.close();
}
}
- 服务端改写
package com.cook.Tct;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
//练习3
public class Server3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
//1.创建Socket对象并绑定端口
ServerSocket socket = new ServerSocket(10009);
//2.等待客户端的连接(如果有客户端连接将返回客户端的连接对象)
while (true) {
final Socket socket1 = socket.accept();
//开启一条线程
//一个用户就对应服务器的一条线程
new Thread(new MyThread(socket1)).start();//有一个客户端来连接就开启一个线程
}
//socket.close();让服务器不关闭
}
}
- 线程类
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.util.UUID;
public class MyThread implements Runnable{
Socket socket1;
public MyThread (Socket socket1){
this.socket1 = socket1;
}
@Override
public void run() {
//使用构造方法将将socket1传递过来
//3.从连接通道中读取数据+将读取的数据保存到本地文件中
try {
BufferedInputStream bis = new BufferedInputStream(socket1.getInputStream());//获取输入流并进行包裹
final String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("C:\\dest1\\"+name+".jpg"));//这里需要完整文件名
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket1.getOutputStream()));
bw.write("上传成功");
bw.newLine();//写入换行
bw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//5.释放资源
if(socket1 != null){
try {
socket1.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
频繁创建线程和消除线程浪费资源,可以使用线程池来进行优化
- 客户端没有改变
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.spec.RSAOtherPrimeInfo;
//练习3
public class Client3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
/*
思路:从本地文件读取数据到内存,然后由客户端发送的服务器
*/
//1.创建Socket对象并连接服务器
Socket socket = new Socket("127.0.0.1",10009);
//2.将文件读取到内存中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\dest\\clent.jpg"));
//3.通过Socket对象获取输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());//将输出流包装成缓冲流
//4.同时进行从文件中读取数据和将数据写入到服务端的操作
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);//将数组中的有效数据都进行写入(不包含残余数据)
}
//5.往服务器写出结束标记
socket.shutdownOutput();
//6.接收服务器回写数据
//获取连接通道的字节流,将其转成字符流然后由缓冲流进行包裹
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
final String line = br.readLine();
System.out.println(line);
//7. 释放资源
socket.close();
}
}
- 服务端使用线程池进行优化
package com.cook.Tct;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//练习3
public class Server3 {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器,接收服务器的反馈
//服务器:接收客户端上传的文件,上传完毕之后给出反馈
//1.创建Socket对象并绑定端口
ServerSocket socket = new ServerSocket(10009);
//***优化(线程池)***
//创建线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
16,//线程池总大小
60,//空闲时间
TimeUnit.SECONDS,//空闲时间单位
new ArrayBlockingQueue<>(2),//阻塞队列
Executors.defaultThreadFactory(),//线程工厂,让线程池创建线程对象
new ThreadPoolExecutor.AbortPolicy()//好像是拒绝策略
);
//2.等待客户端的连接(如果有客户端连接将返回客户端的连接对象)
while (true) {
final Socket socket1 = socket.accept();
//开启一条线程
//一个用户就对应服务器的一条线程
// new Thread(new MyThread(socket1)).start();//有一个客户端来连接就开启一个线程
pool.submit( new MyThread(socket1));
}
//socket.close();让服务器不关闭
}
}
- 线程类没有改变
package com.cook.Tct;
import java.io.*;
import java.net.Socket;
import java.util.UUID;
public class MyThread implements Runnable{
Socket socket1;
public MyThread (Socket socket1){
this.socket1 = socket1;
}
@Override
public void run() {
//使用构造方法将将socket1传递过来
//3.从连接通道中读取数据+将读取的数据保存到本地文件中
try {
BufferedInputStream bis = new BufferedInputStream(socket1.getInputStream());//获取输入流并进行包裹
final String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("C:\\dest1\\"+name+".jpg"));//这里需要完整文件名
byte[]bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket1.getOutputStream()));
bw.write("上传成功");
bw.newLine();//写入换行
bw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//5.释放资源
if(socket1 != null){
try {
socket1.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
练习7(BS架构模型)
对于客户端就是我们的浏览器,而服务端我们的也不用重写,直接用我们的多发和接收举例的就可以了
在浏览器中我们输入 127.0.0.1:10006就可以了(冒号必须是英文的)
- 服务端代码
package com.cook.Tct;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//练习1
public class Server1 {
public static void main(String[] args) throws IOException {
//实现多次接收数据并打印
//1.获取SocketSever对象并绑定端口
ServerSocket ss = new ServerSocket(10006);
//2.监听客户端的连接,成功则返回服务端对象
final Socket socket = ss.accept();
//3.获取输入流对象
final InputStream is = socket.getInputStream();
InputStreamReader ir = new InputStreamReader(is);//经字节流包装成字符流包装对中文的读出
int b;
while ((b = ir.read()) != -1){
System.out.print((char) b);
}
//4.关闭资源
ss.close();
is.close();
}
}
- 服务端接收到的代码如下:
对于给浏览器回写数据需要用到前端的知识
网络编程课后大作业(重点)
有一个用户发送数据,我们服务器接收到后需要给所以的用户都发送一次,执行一个转发的功能