网络编程
基本的通信架构
- 基本的通信架构有2种形式:CS架构( Client客户端/Server服务端 ) 、 BS架构(Browser浏览器/Server服务端)。
- Client-Server(CS)架构:CS均需要程序员开发
- Browser-Server(BS)架构:B不需要程序员开发,只需用户下载安装浏览器,S需程序员开发
网络通信三要素
IP
- 公网IP:是可以连接到互联网的IP地址;
- 内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址;例如,192.168. 开头的就是常见的局域网地址,范围为192.168.0.0–192.168.255.255,专门为组织机构内部使用。
- 本机IP: 127.0.0.1、localhost:代表本机IP,只会寻找当前程序所在的主机
- IP常用命令:
- ipconfig:查看本机IP地址。
- Ipconfig /all: 可查看物理地址
- ping IP地址:检查网络是否连通。
InetAddress:
代表IP地址。
InetAddress的常用方法
小结:
- 说说网络通信至少需要几个要素?
IP、端口、协议 - IP地址是做什么的,具体有几种?
定位网络上的设备的,有IPv4 , IPv6 - 如何查看本机IP地址,如何看是否与对方互通?
ipconfig
ping 192.168.10.23 - 本机IP是谁?
127.0.0.1或者是localhost - Java中,哪个类代表IP地址?
InetAddress - 怎样获得本机IP对象?
InetAddress ip1 = InetAddress.getLocalHost();
端口
端口分类
- 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序。
- 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。小结
- 端口号的作用是什么?
唯一标识正在计算机设备上运行的进程(程序) - 一个设备中,能否出现2个应用程序的端口号一样,为什么?
不可以,如果一样会出现端口冲突错误。
协议
- 通信协议是什么?
计算机网络中,连接和通信数据的规则被称为网络通信协议。 - UDP协议有什么特点?
- 用户数据报协议(User Datagram Protocol)
- UDP是面向无连接,不可靠传输的通信协议。
- 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据。
- TCP协议有哪些特点?
- TCP是一种面向连接的可靠通信协议
- 传输前,采用“三次握手”方式建立连接,点对点的通信。
- 在连接中可进行大数据量的传输。
- 传输后,采用“四次挥手”方式断开连接,确保消息全部收发完毕。
- 通信效率相对较低,可靠性相对较高。
UDP通信(视频直播,语音通话)
快速入门
- 特点:无连接、不可靠通信。
- 不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。
- Java提供了一个java.net.DatagramSocket类来实现UDP通信。
使用UDP通信实现:发送消息、接收消息
客户端实现步骤:
- 创建DatagramSocket对象(客户端对象) —–>扔韭菜的人
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)—–>韭菜盘子
- 使用DatagramSocket对象的send方法,传入DatagramPacket对象—–>开始抛出韭菜
- 释放资源
代码:
package com.carat.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
//客户端
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动了...");
//目标:完成UDP通信一发一收:客户端开发
//1.创建发送端对象(代表抛韭菜的人)
DatagramSocket socket = new DatagramSocket();//随机端口
//2.创建数据包对象封装要发送的数据(韭菜盘子)
byte[] bytes = "我是客户端,约你今晚喝啤酒,龙虾和小烧烤".getBytes();
/**
* 参数一:发送的数据,字节数组(韭菜)
* 参数二:发送的字节长度
* 参数三:目的地的IP地址
* 参数四:服务端程序的端口号
*/
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(),8080);//自己跟自己通信
//3.让发送端对象发送数据包的数据
socket.send(packet);
}
}
服务端实现步骤:
- 创建DatagramSocket对象并指定端口(服务端对象)—–>接韭菜的人
- 创建DatagramPacket对象接收数据(数据包对象) —–>韭菜盘子
- 使用DatagramSocket对象的receive方法,传入DatagramPacket对象—–>开始接收韭菜
- 释放资源
代码:
package com.carat.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.net.SocketException;
//服务端
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
//目标:完成UDP通信一发一收:服务端开发
//1.创建接受端对象,注册端口(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8080);
//2.创建一个数据包对象负责接收数据(韭菜盒子)
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
//3.接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet);
//4.看看数据是否收到了
//获取当前的数据长度
int length = packet.getLength();
String sring = new String(bytes,0,length);
System.out.println(("服务端收到了:" + sring));
//获取对方的ip对象
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + " 对方端口:" + port);
}
}
小结:
实现UDP通信,如何创建客户端、服务端对象?
- public DatagramSocket():创建发送端的Socket对象
- public DatagramSocket(int port):创建接收端的Socket对象
数据包对象是哪个?
- DatagramPacket
如何发送、接收数据?
- 使用DatagramSocket的如下方法:
- public void send(DatagramPacket dp):发送数据包
- public void receive(DatagramPacket dp) :接收数据包
多发多收
客户端可以反复发送数据
客户端实现步骤:
- 创建DatagramSocket对象(发送端对象)
- 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
- 如果用户输入的不是exit, 把数据封装成DatagramPacket
- 使用DatagramSocket对象的send方法将数据包对象进行发送
- 释放资源
代码:
package com.carat.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
//客户端
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动了...");
//目标:完成UDP通信多发多收:客户端开发
//1.创建发送端对象(代表抛韭菜的人)
DatagramSocket socket = new DatagramSocket();//随机端口
Scanner sc = new Scanner(System.in);
while (true) {
//2.创建数据包对象封装要发送的数据(韭菜盘子)
System.out.println("请输入要发送的内容:");
String msg = sc.nextLine();//nextLine可以把空格也发送出去.而next会省略空格
//如果用户输入的"exit",则会退出
if("exit".equals(msg)){
System.out.println("客户端退出了...");
socket.close();//关闭资源
break;//退出循环
}
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(),8081);//自己跟自己通信
//3.让发送端对象发送数据包的数据
socket.send(packet);
}
}
}
输出结果:
接收端可以反复接收数据
接收端实现步骤:
- 创建DatagramSocket对象并指定端口(接收端对象) —–>接韭菜的人
- 创建DatagramPacket对象接收数据(数据包对象) —–>韭菜盘子
- 使用DatagramSocket对象的receive方法传入DatagramPacket对象—–>开始接受韭菜
- 使用while死循环不断的进行第3步
代码:
package com.carat.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
//服务端
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
//目标:完成UDP通信多发多收:服务端开发
//1.创建接受端对象,注册端口(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8081);
//2.创建一个数据包对象负责接收数据(韭菜盒子)
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
while (true) {
//3.接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet);//等待式接收数据
//4.看看数据是否收到了
int length = packet.getLength();
String sring = new String(bytes,0,length);
System.out.println(("服务端收到了:" + sring));
//获取对方的ip对象
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + " 对方端口:" + port);
System.out.println("--------------------------------------");
}
}
}
输出结果:
小结:
UDP的接收端为什么可以接收很多发送端的消息?
- 接收端只负责接收数据包,无所谓是哪个发送端的数据包。
TCP通信(网页,文件下载,支付)
快速入门
- 特点:面向连接、可靠通信。
- 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
- Java提供了一个java.net.Socket类来实现TCP通信。
TCP通信的实现一发一收-客户端开发
客户端程序就是通过java.net包下的Socket类来实现的。
客户端实现步骤:- 创建客户端的Socket对象,请求与服务端的连接。
- 使用socket对象调用getOutputStream()方法得到字节输出流。
- 使用字节输出流完成数据的发送。
- 释放资源:关闭socket管道。
代码:
package com.carat.demo4tcp1;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.DatagramSocket;
import java.net.Socket;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信下一发一收:客户端开发
//1.常见Socket管道对象,请求与服务器的Socket连接.可靠连接
System.out.println("客户端启动了....");
Socket socket = new Socket("127.0.0.1",9999);
//2.从Socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
//3.把字节输出流包装成特殊数据输出流
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(1);
dos.writeUTF("我想你了,你在哪儿?");
//4.释放资源
socket.close();
}
}
TCP通信的实现一发一收-服务端开发
服务端是通过java.net包下的ServerSocket类来实现的。
服务端实现步骤:
- 创建ServerSocket对象,注册服务端端口。
- 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
- 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
- 释放资源:关闭socket管道
代码:
package com.carat.demo4tcp1;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信下一发一收:服务端开发
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
System.out.println("服务端启动了....");
ServerSocket ss = new ServerSocket(9999);
//2.调用accept方法,阻塞等待客户端连接,一旦有客户端连接就会返回一个Socket对象
Socket socket = ss.accept();
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
//5.读取数据
int id = dis.readInt();
String msg = dis.readUTF();
System.out.println("id:" + id + ",收到的客户端msg:" + msg);
//6.客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口" + socket.getPort());
}
}
输出结果:
小结:
TCP通信,客户端的代表类是谁?
- Socket类
- public Socket(String host , int port)
TCP通信,如何使用Socket管道发送、接收数据 ?
- OutputStream getOutputStream():获得字节输出流对象(发)
- InputStream getInputStream():获得字节输入流对象(收)
TCP通信服务端用的类是谁?
- ServerSocket类,注册端口。
- 调用accept()方法阻塞等待接收客户端连接。得到Socket对象。
多发多收
使用TCP通信实现:多发多收消息
步骤:
- 客户端使用死循环,让用户不断输入消息。
- 服务端也使用死循环,控制服务端程序收完消息后,继续去接收下一个消息。
客户端代码:
package com.carat.demo5txp2;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信下多发多收:客户端开发
//1.常见Socket管道对象,请求与服务器的Socket连接.可靠连接
System.out.println("客户端启动了....");
Socket socket = new Socket("127.0.0.1",9999);
//2.从Socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
//3.把字节输出流包装成特殊数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送的数据:");
String msg = sc.nextLine();
if("exit".equals(msg)){
System.out.println("退出成功!");
dos.close();//关闭输出流
socket.close();//关闭socket
break;
}
dos.writeUTF(msg);//发送数据
dos.flush();//刷新缓冲区
}
//4.释放资源
}
}
服务端代码:
package com.carat.demo5txp2;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信下多发多收:服务端开发
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
System.out.println("服务端启动了....");
ServerSocket ss = new ServerSocket(9999);
//2.调用accept方法,阻塞等待客户端连接,一旦有客户端连接就会返回一个Socket对象
Socket socket = ss.accept();
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
//5.读取数据
String msg = dis.readUTF();//等待读取客户端发送的数据
System.out.println("收到的客户端msg:" + msg);
//6.客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口" + socket.getPort());
System.out.println("------------------------------------");
}
}
}
输出结果:
小结:
本次多发多收是如何实现的?
- 客户端使用循环反复地发送消息。
- 服务端使用循环反复地接收消息。
同时接收多个客户端的消息(核心)
客户端代码不变
服务器代码(有上线下线功能)
package com.carat.demotcp3;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信下多发多收:服务端开发 支持多个客户端开发
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
System.out.println("服务端启动了....");
ServerSocket ss = new ServerSocket(9999);
while (true) {
//2.调用accept方法,阻塞等待客户端连接,一旦有客户端连接就会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
//3.把这个客户管道交给一个独立的子线程专门负责接收这个管道的消息
new ServerReader(socket).start();
}
}
}
客户端子线程(上线下线功能):
package com.carat.demotcp3;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class ServerReader extends Thread{
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//读取管道的消息
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
//5.读取数据
String msg = dis.readUTF();//等待读取客户端发送的数据
System.out.println("收到的客户端msg:" + msg);
//6.客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口" + socket.getPort());
System.out.println("------------------------------------");
}
} catch (Exception e) {
System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
}
}
}
输出结果:
小结:
本次是如何实现服务端同时接收多个客户端的消息的?
- 主线程定义了循环负责接收客户端Socket管道连接
- 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
其他应用:B/S架构的原理
HTTP协议规定:响应给浏览器的数据格式必须满足如下格式:
每次请求都开一个新线程,高并发时容易宕机
使用线程池进行优化:
代码:
服务端创建线程池:
package com.carat.demo7tcp4;
import com.carat.demo6tcp3.ServerReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class ServerDemo {
public static void main(String[] args) throws Exception {
//目标:B/S架构的原理理解
System.out.println("服务端启动了....");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8080);
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(3, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
while (true) {
//2.调用accept方法,阻塞等待客户端连接,一旦有客户端连接就会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
//3.把这个客户管道包装成一个任务交给线程池处理
pool.execute(new ServerReaderRunnable(socket));
}
}
}
Runnable任务对象:
package com.carat.demo7tcp4;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//给当前对应的浏览器管道响应一个网页数据回去
OutputStream os = socket.getOutputStream();
//通过字节输出流包装写出去的数据给客户端
//把字节输出流包装成打印输出流
PrintStream ps = new PrintStream(os);
//写响应的网页数据出去
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println();//必须换一行
ps.println("<html>");
ps.println("<head>");
ps.println("<meta charset='utf-8'>");
ps.println("<title>");
ps.println("服务器响应数据");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1 style='color:red;font-size=20px'>服务器响应数据</h1>");
//展示一个logo图片
ps.println("<img src='https://www.itheima.com/images/logo.png'>");
ps.println("</body>");
ps.println("</html>");
ps.close();
socket.close();
} catch (Exception e) {
System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
}
}
}
输出结果:
面试考点:
模拟写过一个B/S架构的网站用过线程池,把所有的socket客户端管道包装成runnable任务对象,让她们成为任务,进入到任务队列里,交给线程池,由核心线程来处理,线程处理一个任务,就响应一个网页给浏览器展示.
小结:
BS架构的基本原理是什么?
- 客户端使用浏览器发起请求(不需要开发客户端)
- 服务端必须按照HTTP协议响应数据
来自湖南