网络编程
计算机和计算机之间数据通过网络传输。
常见软件架构
CS : (ClientServer 客户端/服务器) 在用户本地需要下载并安装客户端程序,在远程有一个服务器端程序。如:QQ,Steam
BS :(Browser/Server 浏览器/服务器) 只需要一个浏览器啊,用户可通过不同的网址,访问不同的服务器。 如:淘宝,京东
网络编程三要素
IP : 设备在网络中的地址(唯一标识)
- IPV4 :互联网通信协议第四版(只有42亿数量已经分配完)
- IPV6 : 解决IPV4不够用的问题
- 特殊IP地址 : 127.0.0.1,也可以是localhost: 是回送地址也称本地回环地址。也称本地IP。如,向IP 127.0.0.1发送数据,是不经过路由器的,直接发送到本地。
端口号 : 应用程序在设备中唯一的标识(取值范围:0-65535,其中0-1023之间的端口号用于一些知名网络服务或者应用,我们自己使用1024以上即可)
- 一个端口号只能被一个应用程序使用
协议 : 数据在网络中传输的规则,常见协议有UDP,TCP,HTTP,HTTPS,FTP
InetAddress类
public class MyInetAddressDemo1 {
public static void main(String[] args) throws UnknownHostException {
//获取InetAddress对象
//IP的对象 一台电脑的对象(IP唯一)
InetAddress address = InetAddress.getByName("LAPTOP-8QI8KDR8");
System.out.println(address);
//获取IP地址
String ip = address.getHostAddress();
System.out.println(ip);
//获取主机名
String hostName = address.getHostName();
System.out.println(hostName);
}
}
因为电脑的IP地址唯一,获取了IP对象可以视为获取的电脑的对象。
Inet4Address,Inet6Address为InetAddress的子类,在获取IP对象的时候,会自动判断是IPv4还是IPv6,并创建。
协议
TCP/IP参考模型
每一层都有自己的协议
应用层 :HTTP,FTP,DNS…
传输层 :TCP,UDP…
网络层 : IP,ICMP…
物理链路层 : 硬件设备
数据发送从应用层到物理链路层一层层转换,接收方电脑从物理链路层向上解析到应用层。
UDP协议
用户数据协议
- UDP是面向无连接通信协议 速度快,有大小限制,一次最多发送64K,数据不安全容易丢失数据。(不会去确定是否有链接便直接发送消息)
- 适用于丢一点数据无所谓的场景,如视频通话
UDP通信程序
发送数据
1.创建发送端的DatagramSocket对象
2.数据打包
3.发送数据
4.释放资源
public class SendMessageDemo {
public static void main(String[] args) throws IOException {
//创建DatagramSocket对象
DatagramSocket ds = new DatagramSocket();//可绑定端口,不写则为随机端口使用
//打包数据
String str = "hello,UDP";
byte[] bytes = str.getBytes();//转为字节数组
InetAddress address = InetAddress.getByName("127.0.0.1");//发送给该电脑
int port = 10086;//发送到该端口号
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);
ds.send(datagramPacket);//发送数据
ds.close();
}
}
接收数据
1.创建DatagramSocket对象,绑定端口,且要与发送的端口一致
2.接收数据包
3.解析数据包
4.释放资源
public class ReceiveMessageDemo {
public static void main(String[] args) throws IOException {
//创建DatagramSocket对象,绑定端口,且要与发送的端口一致
DatagramSocket datagramSocket = new DatagramSocket(10086);
//接收数据包
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
//阻塞式方法,等待接收数据
datagramSocket.receive(dp);
//解析数据包
byte[] data = dp.getData();
int length = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();
System.out.println("接收到的数据:" + new String(data, 0, length));
System.out.println("发送方的IP地址:" + address.getHostAddress() + ", 端口号:" + port);
//释放资源
datagramSocket.close();
}
}
测试样例:
public class ReceiveMessage {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10086);
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
while (true) {
ds.receive(dp);
//第二次接收数据时,会将第一次接收的数覆盖
byte[] data = dp.getData();
int length = dp.getLength();
int port = dp.getPort();
String hostAddress = dp.getAddress().getHostAddress();
String hostName = dp.getAddress().getHostName();
System.out.println("hostAddress: "
+ hostAddress
+ ", hostName: "+ hostName
+ ", port: " + port
+ ", length: " + length
+ ", data: " + new String(data, 0, length));
}
}
}
public class SendMessage {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("Enter the message to send: ");
String str = sc.nextLine();
if("886".equals(str)){
System.out.println("Connection closed");
break;
}
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("localhost");
int port = 10086;
ds.send(new DatagramPacket(bytes, bytes.length, address, port));
}
ds.close();
}
}
测试:
Enter the message to send:
yg我爱你
Enter the message to send:
muamua
Enter the message to send:
886
Connection closed
打印结果:
hostAddress: 127.0.0.1, hostName: 127.0.0.1, port: 58751, length: 11, data: yg我爱你
hostAddress: 127.0.0.1, hostName: 127.0.0.1, port: 58751, length: 6, data: muamua
UDP的三种通信方式
1.单播:一对一
2.组播:发送到组播地址(可以表示多个电脑):224.0.0.0–239.255.255.255
其中224.0.0.0–224.0.0.255为预留的组播地址,可以我们自己使用
public class SendMessageDemo {
public static void main(String[] args) throws IOException {
//创建MulticastSocket
MulticastSocket ms =new MulticastSocket();
//创建DatagramPacket
String str = "yg我爱你啾咪";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1");//需指定组播地址
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
ms.send(dp);
ms.close();
}
}
public class ReceiveMessageDemo1 {
public static void main(String[] args) throws IOException {
MulticastSocket ms =new MulticastSocket(10086);
//将当前本机添加到224.0.0.1的组播组中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
ms.receive(datagramPacket);
System.out.println(new String(datagramPacket.getData(),0,datagramPacket.getLength()));
}
}
public class ReceiveMessageDemo2 {
public static void main(String[] args) throws IOException {
MulticastSocket ms =new MulticastSocket(10086);
//将当前本机添加到224.0.0.1的组播组中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
ms.receive(datagramPacket);
System.out.println(new String(datagramPacket.getData(),0,datagramPacket.getLength()));
}
}
public class ReceiveMessageDemo3 {
public static void main(String[] args) throws IOException {
MulticastSocket ms =new MulticastSocket(10086);
//将当前本机添加到224.0.0.1的组播组中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
ms.receive(datagramPacket);
System.out.println(new String(datagramPacket.getData(),0,datagramPacket.getLength()));
}
}
3.广播: 给局域网中所有电脑发送数据
TCP协议
传输控制协议
- TCP协议是面向连接协议
- 速度慢,无大小限制,数据安全(确认连接再发送,不会造成数据丢失)如:下载软件,文字聊天
TCP通信程序
它在两端各建立一个Socket对象,通信之前保证链接已经建立,通过Socket产生IO流来进行网络通信。
客户端 ----------> (通信之前保证链接已经建立)---------> 服务器
(Socket) 输出流 输入流 (ServerSocket)
1.创建客户端的Socket对象与指定服务端连接 1.创建服务器端的Socket对象
2.获取输出流,写数据 2.监听客户端连接,返回一个Socket对象
3.释放资源 3.获取输入流,读数据,并把数据显示在 控制台
4.释放资源
Client:
public class Client {
public static void main(String[] args) throws IOException {
//TCP协议,发送数据
//创建对象的同时会连接服务器,连接失败会报错
// 此处连接包含三次握手协议保证链接建立
Socket socket = new Socket("127.0.0.1", 10086);
//连接成功,服务端返回连接对象
//从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
os.write("yg i love u".getBytes());
os.close();//流存在于连接通道中,若关闭了通道,流也会一并关闭,可以不写。
socket.close();//包含四次挥手协议,利用这个协议断开连接,保证断开连接时连接通道中数据被处理完毕
}
}
Server:
public class Server {
public static void main(String[] args) throws IOException {
//TCP协议,接收数据
ServerSocket ss = new ServerSocket(10086);
//监听客户端的链接
Socket socket = ss.accept();//服务端创建后,程序会阻塞在这里监听,直到有客户端连接,并返回连接对象
//从连接通道获取输入流,读数据
InputStream is = socket.getInputStream();
int b;
while ((b = is.read()) != -1) {
//字节转换为字符
System.out.print((char) b);
}//在循环中,is.read() 方法用于读取文件中的一个字节,并将其存储在变量 b 中。当 b 不等于 -1 时,表示读取到了文件中的一个字节,而不是读取到了文件末尾。
//释放资源
socket.close();
ss.close();
}
}
此时只能传输纯英文字符,传输中文会出现乱码。因为此时未指定编码表,将会把一个中文拆成3个字节,并将字节传输到服务器,字节流一次读一个字节并且转为字符,无法转成字符。需改成字符流
修改后的Server:
public class Server {
public static void main(String[] args) throws IOException {
//TCP协议,接收数据
ServerSocket ss = new ServerSocket(10086);
//监听客户端的链接
Socket socket = ss.accept();//服务端创建后,程序会阻塞在这里监听,直到有客户端连接,并返回连接对象
//从连接通道获取输入流,读数据
InputStream is = socket.getInputStream();\
//转成字符流
InputStreamReader isr = new InputStreamReader(is);
//添加缓冲流
BufferedReader br = new BufferedReader(isr);
int b;
while ((b = br.read()) != -1) {
//字节转换为字符
System.out.println((char) b);
}//在循环中,br.read() 方法用于读取文件中的一个字节,并将其存储在变量 b 中。当 b 不等于 -1 时,表示读取到了文件中的一个字节,而不是读取到了文件末尾。
//释放资源
socket.close();//关闭连接
ss.close();//关闭服务器
}
}
额外:
回写数据
当服务器向客户端回写数据时,对于服务器来讲,是输出,要用输出流。客户端接收回写数据时,是输入流。
客户端:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",10086);
OutputStream os = socket.getOutputStream();
String str = "熬哦 熬哦";
os.write(str.getBytes());
//结束标记
socket.shutdownOutput();
//接收服务器回写数据
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while((b = isr.read()) != -1){
System.out.print( (char)b );
}
socket.close();
}
}
服务器:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10086);
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while((b = isr.read()) != -1){
//细节:read方法会从通道中读取数据
//未读到结束标记时将会继续回到read方法,等待数据
//需要有结束标记,循环才能停止,进入下面的回写数据
System.out.print( (char)b );
}
//回写数据
OutputStream os = socket.getOutputStream();
String str = "雪豹闭嘴";
os.write(str.getBytes());
socket.close();
ss.close();
}
}
文件传输
Client :
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 12345);
//获取本地文件,并传入服务器中
//若文件较大,直接用字节流将不合适,一个字节一个字节传输太慢了
//可使用缓冲流包裹
BufferedInputStream bufferedInputStream = new BufferedInputStream(
new FileInputStream("F:\\java代码\\JavaJC\\socketnet\\image\\社区头像1.jpg")
);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while ((len = bufferedInputStream.read(bytes)) != -1) {
bufferedOutputStream.write(bytes, 0, len);
}
//将缓冲区的内容刷新到磁盘,确保数据被写入到文件中。
bufferedOutputStream.flush();
//写出结束标记
socket.shutdownOutput();
//接受服务器回写数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println(line);
socket.close();
}
}
Server :
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(12345);
Socket socket = ss.accept();
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\java代码\\JavaJC\\socketnet\\target\\test.jpg"));
int len;
byte[] bytes = new byte[1024];
while( (len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("文件发送成功");
bw.newLine();
bw.flush();
bos.close();
socket.close();
ss.close();
}
}
注意:数据传输经过连接通道时,必须将缓冲区内容刷新到磁盘再关闭连接,否则可能会导致有数据没有被写入磁盘,导致文件传输不完整。
多线程版本的文件传输服务端:
利用循环使每次传输文件都开启一条线程去执行
改写Server:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(12345);
while (true) {
Socket socket = ss.accept();
//将socket传入Myrunnable
new Thread(new MyRunnable(socket)).start();
}
}
}
创建MyRunnable:
public class MyRunnable implements Runnable{
Socket socket;
public MyRunnable(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
//使用UUID生成唯一ID防止文件名重复
String name = UUID.randomUUID().toString().replace("-","");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\java代码\\JavaJC\\socketnet\\target\\"+name+".jpg"));
int len;
byte[] bytes = new byte[1024];
while( (len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("文件发送成功");
bw.newLine();
bw.flush();
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
反复创建线程池且消费十分浪费资源,使用线程池优化
public class Server {
public static void main(String[] args) throws IOException {
//创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5,//核心线程数量
10,//线程池总大小
60,//空闲时间
TimeUnit.SECONDS,//空闲时间单位
new ArrayBlockingQueue<>(10),//仍无队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//拒绝策略
);
ServerSocket ss = new ServerSocket(12345);
while (true) {
Socket socket = ss.accept();
//将socket传入Myrunnable
//new Thread(new MyRunnable(socket)).start();
//任务提交到线程池
pool.submit(new MyRunnable(socket));
}
}
}