网络编程
- 网络通信
- 网络通信三要素之IP地址(了解)
- JAVA对IP地址的操作(InetAddress类)
- 网络通信三要素之端口号(了解)
- 网络通信三要素之协议(了解)
- UDP通信
- UDP通信之广播和组播(了解)
- TCP通信
- TCP通信实战案例-即时通信
- BS架构模拟
- URL了解
网络编程可以让程序与网络上的其他设备中的程序进行数据交互
所以,我们学习网络编程的主要目的就是为了实现网络通信
网络通信
网络通信基本模式
常见的通信模式有如下2种形式: Client-Server(Cs)、Browser/Server(Bs)
Client-Server(Cs)主要是客户端与服务端之间的联系(就是相应的App和后台的关系)
模型
Browser/Server(Bs)主要是浏览器与服务端的联系(即不需要下载App,只需下载浏览器即可)
模型
实现网络编程关键的三要素
IP地址: 设备在网络中的地址,是唯一的标识
端口:应用程序在设备中唯一的标识
协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
即我们要实现与某人通信,需要知道对方的IP地址,对方用于联系软件(应用程序)的端口号(不同电脑上的同一软件的端口号一般相同),再通过相关协议实现消息的传递,即实现了网络通信。
模型图如下
网络通信三要素之IP地址(了解)
IP地址
IP (Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志
常见的IP分类为:IPV4和IPv6
IPV4:
IPV4由32位(4字节)组成,底层由四个字节组成,以点分十进制表示法来展现
例如:11000000 10101000 00000001 01000010——>192.168.1.66
(每一个字节是一组数,以“ . ”隔开)
流程模型
IPV6:(了解一下,不需要掌握)
- IPv6由128位 (16个字节),号称可以为地球每一粒沙子编号
- IPv6分成8个整数,每个整数用四个十六进制位表示,数之间用冒号 (:)分开(冒分十六进制表示法)
IP地址形式:
公网地址 和 私有地址(局域网使用)
192.168.开头的就是常见的局域网地址,范用即为192.168.0.0–192.168.255.255,专门为组织机构内部使用
IP常用命令:
ipconfig:查看本机IP地址
ping IP地址:检查网络是否连通
特殊IP地址:
- 本机IP:127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机
JAVA对IP地址的操作(InetAddress类)
InetAddress 的使用
- 此类表示Internet协议 (IP) 地址
InetAddress API如下
方法名称 | 说明 |
---|---|
public static InetAddress getLocalHost() | 返回本主机的地址对象 |
public static InetAddress getByName(String host) | 得到指定主机的IP地址对象,参数是域名或者IP地址 |
public String getHostName() | 获取此IP地址的主机名 |
public String getHostAddress() | 返回IP地址字符串 |
public boolean isReachable(int timeout) | 在指定毫秒内连通该IP地址对应的主机,连通返回true |
代码展示
public static void main(String[] args) throws Exception{// 获取本机地址对象InetAddress it= InetAddress.getLocalHost();System.out.println(it.getHostName()); //获取主机名System.out.println(it.getHostAddress());//获取主机的IP地址// 获取域名ip对象InetAddress it1=InetAddress.getByName("www.baidu.com");System.out.println(it1.getHostName());//www.baidu.comSystem.out.println(it1.getHostAddress()); //39.156.66.14// 获取公网IP对象(用IP地址作参反应较慢,相当于进行了一个联网操作)InetAddress it2=InetAddress.getByName("39.156.66.14");System.out.println(it2.getHostName());//39.156.66.14System.out.println(it2.getHostAddress());//39.156.66.14//判断是否能通: ping 5s之内测试是否可通System.out.println(it.isReachable(5000));System.out.println(it1.isReachable(5000));System.out.println(it2.isReachable(5000));}
网络通信三要素之端口号(了解)
端口号
- 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范用是 0~65535
端口类型
周知端口:0~1023,被预先定义的知名应用占用(如: HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户进程或某些应用程序。(如: Tomcat占用8080,MVSQL占用3306)
动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错
网络通信三要素之协议(了解)
计算机网络中,连接和通信数据的规则被称为网络通信协议
网络通信协议有两套参考模型
OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广
TCP/IP参考模型(或TCP/IP协议): 事实上的国际标准
我们目前只需掌握TCP和UDP协议即可
传输层的2个常见协议
TCP(Transmission Control Protocol) : 传输控制协议
UDP(User Datagram Protocol): 用户数据报协议
TCP协议:
使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
传输前,采用 “三次握手” 方式建立连接,所以是可靠的
在连接中可进行大数据量的传输
连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低
TCP协议通信场景
- 对信息安全要求较高的场景,例如:文件下载、金融等数据通信
TCP三次握手确立联系模型图
TCP四次挥手断开连接模型图
UDP协议:
UDP是一种无连接、不可靠传输的协议
将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
每个数据包的大小限制在64KB内
发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
可以广播发送,发送数据结束时无需释放资源,开销小,速度快
UDP协议通信场景
- 语音通话,视频会话等
UDP通信
特点
UDP是一种无连接、不可靠传输的协议
将数据源IP、目的地P和端口以及数据封装成数据包,大小限制在64KB内,直接发送出去即可
我们用一个模型来展示,比如说,集市的小吃摊,一个客人点了一份韭菜盒子,那店家要想给他送过去,需要知道客人的方向(IP地址)和具体的桌位(端口号),然后店家根据方位把韭菜盒子送过去,韭菜盒子加方位就相当于一个数据包,而客人只需要准备好餐具(接收端接收数据的容器)开吃即可
模型演示
DatagramSocket:发送端(店家)
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建发送端的Socket对象,系统会随机分配一个端口号(也可以自己填一个端口号,但没必要) |
DatagramSocket类成员方法(店家送韭菜盒子)
方法名称 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包 |
DatagramPacket:数据包对象(店家的韭菜盘子和客人的具体方位)
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 创建发送端数据包对象:buf:要发送的内容,字节数组;length: 要发送内容的字节长度;address: 接收端的IP地址对象;port:接收端的端口号 |
DatagramSocket:接收端(客人)
构造器 | 说明 |
---|---|
public DatagramSocket(int port) | 创建接收端的socket对象并指定端口号 |
DatagramSocket类成员方法(客人收到韭菜盒子)
方法名称 | 说明 |
---|---|
public void receive(DatagramPacket p) | 接收数据包 |
DatagramPacket:数据包对象(客人准备好餐具开吃)
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf, int length) | 创建接收端的数据包对象:buf:用来存储接收的内容;length: 能够接收内容的长度 |
DatagramPacket常用方法(客人查看韭菜盒子的个数)
方法名称 | 说明 |
---|---|
public int getLength() | 获得实际接收到的字节个数 |
店家代码展示
public class DatagramClient {public static void main(String[] args) throws Exception{DatagramSocket socket=new DatagramSocket();byte[] plate="单身好辛苦~~~".getBytes();DatagramPacket packet=new DatagramPacket(plate,plate.length,InetAddress.getLocalHost(),6666);socket.send(packet);socket.close();}}
客人代码展示
public class DatagramService {public static void main(String[] args) throws Exception{DatagramSocket socket=new DatagramSocket(6666);byte[] plate=new byte[64*1024];DatagramPacket packet=new DatagramPacket(plate,plate.length);socket.receive(packet);System.out.println(new String(plate,0,packet.getLength()));socket.close();}}
(一定要先开启服务端(店家),再开启客户端(客人),不然送不到
原因是这样的,我们开启了服务端,服务端就在等待客户端与它建立联系,如果先开启客户端,客户端并不需要等待与其建立联系,就一直跑完了,就相当于店家开张,等待客人,和店家忘了开张,客人一去发现没看们就走了)
以上只实现了一发一收,我们可以加一个死循环实现多发多收
任务:实现多发多收,当客户端输入“exit”是,结束通信
介绍两个DatagramPacket方法(了解即可)
方法名称 | 说明 |
---|---|
public synchronized SocketAddress getSocketAddress() | 返回此数据包的远程主机的套接字地址(通常为 IP 地址 + 端口号) 正在发送到或来自 |
public synchronized InetAddress getAddress() | 返回此数据报所在的计算机的 IP 地址 已发送或从中接收数据报,或者如果没有返回null |
public synchronized int getPort() | 返回此数据报所在的远程主机上的端口号 正在发送或从中接收数据报,如果未设置,则为 0。 |
服务端代码展示
public class DatagramService {public static void main(String[] args) throws Exception{DatagramSocket socket=new DatagramSocket(6666);byte[] plate=new byte[64*1024];while (true) {DatagramPacket packet=new DatagramPacket(plate,plate.length);socket.receive(packet);System.out.println(new String(plate,0,packet.getLength()));}}}
客户端代码展示
public class DatagramClient {public static void main(String[] args) throws Exception{DatagramSocket socket=new DatagramSocket();Scanner sc=new Scanner(System.in);while (true) {System.out.println("请输入:");String say=sc.next();if ("exit".equals(say)){socket.close();break;}else {byte[] plate=say.getBytes();DatagramPacket packet=new DatagramPacket(plate,plate.length,InetAddress.getLocalHost(),6666); socket.send(packet);}}}}
我们也可以实现并发操作,即开启多个客户端
我的idea是JDK17
①在左上方有这个标志,点击一下,再点第一个选项
②点击Modify options
③点击第一个即可
问题
UDP的接收端为什么可以接收很多发送端的消息” />
UDP通信之广播和组播(了解)
广播
UDP要实现广播,需使用广播地址:255.255.255.255
具体操作:
- ①发送端发送的数据包的目的地写的是广播地址、且指定端口(255.255.255.255,端口号)
- ②本机所在网段的其他主机的程序只要匹配端口成功即就可以收到消息了
组播
UDP要实现组播,需使用组播地址: 224.0.0.0~ 239.255.255.255
具体操作:
①发送端的数据包的目的地是组播IP (例如: 224.0.1.1,端口: 9999)
②接收端必须绑定该组播IP(224.0.1.1),端口还要对应发送端的目的端口(9999),这样即可接收该组播消息
③DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP
MulticastSocket的使用
JDK14的使用方法(因为14开始就过时了)
public static void main(String[] args) throws Exception{MulticastSocket socket=new MulticastSocket(7777);socket.joinGroup(InetAddress.getByName("224.0.0.1"));}
14开始建议使用方法
public static void main(String[] args) throws Exception{MulticastSocket socket=new MulticastSocket(7777);socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.0.1"),7777),NetworkInterface.getByInetAddress(InetAddress.getLocalHost())); }
参数一:需要新建一个InetSocketAddress对象,在该对象的参数里填入组播IP和端口,该端口号与目的端口号是一致的
参数二:一般使用默认的
TCP通信
特点
TCP是一种面向连接,安全、可靠的传输数据的协议
传输前,采用“三次握手”方式,点对点通信,是可靠的
在连接中可进行大数据量的传输
TCP通信模式演示:
(由上图可知,TCP是建立一个Socket管道,然后通过IO流实现的)
注意: 在java中只要是使用iava.net.Socket类实现通信,底层即是使用了TCP协议
创建客户端的相关API
代表类:Socket(套接字)
构造器 | 说明 |
---|---|
public Socket(String host , int port) | 创建发送端的Socket对象与服务端连接,参数为服务端程序的IP和端口 |
创建服务端的相关API
ServerSocket(服务端)
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 注册服务端端口 |
但我们建立的管道是Socket管道,所以我们要得到服务端的Socket,用下面的方法
ServerSocket类成员方法
方法名称 | 说明 |
---|---|
public Socket accept() | 等待接收客户端的Socket通信连接,连接成功返回Socket对象与客户端建立端到端通信 |
Socket类成员方法
方法名称 | 说明 |
---|---|
OutputStream getOutputStream() | 获得字节输出流对象(发) |
InputStream getInputStream() | 获得字节输入流对象(收) |
(发消息用输出流,收消息用输入流)
客户端代码
public class SocketClient {public static void main(String[] args) throws Exception{Socket socket=new Socket(InetAddress.getLocalHost(),6666);OutputStream os=socket.getOutputStream();PrintStream ps=new PrintStream(os);ps.println("单身快乐!!!");ps.println("我讨厌单身!!!");ps.println("单身狗~~~");socket.close();}}
服务端代码
public class SocketService {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(6666);Socket socket=ss.accept();InputStream is=socket.getInputStream();BufferedReader sr=new BufferedReader(new InputStreamReader(is));String len;while ((len=sr.readLine())!=null){System.out.println(len);}}}
我们现在只实现了一次性发送(也算单发单收吧),客户端发完后一定要记得关流,否则会错误,如果不关流,把while改为if,但只能接收一条,实现真正的单发单收
TCP通信的基本原理
客户端怎么发,服务端就应该怎么收
客户端如果没有消息,服务端会进入阻塞等待(即在 (len=sr.readLine())!=null 这一行等待
Socket一方关闭或者出现异常、对方Socket也会失效或者出错(失效指关流;出错指不关流,while不改变,即服务端一直等待,但客户端已跑完的情形)
那么,接下来就要实现多发多收了
任务:实现多发多收,当客户端输入“exit”是,结束通信
客户端代码
public class SocketClient {public static void main(String[] args) throws Exception{Socket socket=new Socket(InetAddress.getLocalHost(),6666);OutputStream os=socket.getOutputStream();PrintStream ps=new PrintStream(os);Scanner sc=new Scanner(System.in);while (true) {System.out.println("请输入");String say=sc.next();if ("exit".equals(say)) {socket.close();break;}else {ps.println(say);}}}}
服务端代码
public class SocketService {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(6666);Socket socket=ss.accept();InputStream is=socket.getInputStream();BufferedReader sr=new BufferedReader(new InputStreamReader(is));String len;while ((len=sr.readLine())!=null){System.out.println(len);}}}
那么,接下来,我们就要实现多个客户端发消息了,那么我们可以直接多开吗?
- 不可以的。因为服务端现在只有一个线程,只能与一个客户端进行通信
那么,解决方案就是开多个线程去实现
模型图如下
任务:实现多个客户端发消息
首先,我们要实现多线程
public class MyRunnable implements Runnable{private Socket socket;public MyRunnable(Socket socket){this.socket=socket;}@Overridepublic void run() {try {InputStream is=socket.getInputStream();BufferedReader sr=new BufferedReader(new InputStreamReader(is));String len;while ((len=sr.readLine())!=null){System.out.println(len);}} catch (Exception e) {e.printStackTrace();}}}
(一定要记住,重写的run方法中的内容一定要是与服务端有关的,因为我们可以创建多个客户端,但服务端一次只能实先一个线程)
服务端代码
public class SocketService {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(6666);while (true) {Socket socket=ss.accept();MyRunnable myRunnable=new MyRunnable(socket);Thread t=new Thread(myRunnable);t.start();}}}
我们要用对客户端注册服务端端口这一操作进行死循环,不必担心内存溢出,因为 Socket socket=ss.accept(); 它会停留在这一步,等待与新的客户端相连,不会不停的生成
客户端代码(与以往的一样)
ublic class SocketClient {public static void main(String[] args) throws Exception{Socket socket=new Socket(InetAddress.getLocalHost(),6666);OutputStream os=socket.getOutputStream();PrintStream ps=new PrintStream(os);Scanner sc=new Scanner(System.in);while (true) {System.out.println("请输入");String say=sc.next();if ("exit".equals(say)) {socket.close();break;}else {ps.println(say);}}}}
小总结
本次是如何实现服务端接收多个客户端的消息的
主线程定义了循环负责接收客户端Socket管道连接
每接收到一个Socket通信管道后分配一个独立的线程负责处理它
目前,我们实现多个客户端的连接是一个客户端一个线程,这样在大工程中会占用大量的内存,那么,自然而然的我们会想到多线池对其优化
我们要把目前的实现模型
改为这样
就是把每一个连接管道当成任务,再有线程来接
任务:引入线程池处理多个客户端消息
只需把服务端更改即可
public class SocketService {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(6666);ExecutorService pool=new ThreadPoolExecutor(3,5,2,TimeUnit.HOURS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());while (true) {Socket socket=ss.accept(); pool.execute(new MyRunnable(socket));}}}
本次使用线程池的优势
服务端可以复用线程处理多个客户端,可以避免系统瘫痪
适合客户端通信时长较短的场景
TCP通信实战案例-即时通信
即时通信是什么含义,要实现怎么样的设计” />
那么,服务端除了接数据外也要往外输数据,客户端也要收数据,那么客户端的收数据也要写一个线程(也可以不写,但那样容易晕)
public class MyRunnableReceive implements Runnable{private Socket socket;public MyRunnableReceive(Socket socket){this.socket=socket;}@Overridepublic void run() {try {InputStream is=socket.getInputStream();BufferedReader sr=new BufferedReader(new InputStreamReader(is));String len;while ((len=sr.readLine())!=null){System.out.println(len);}} catch (Exception e) {SocketService.list.remove(socket);}}}
服务端要定义一个集合,一旦接收到一个管道,便把这个管道加入集合中
public class SocketService {protected static List<Socket> list=new ArrayList<>();public static void main(String[] args) throws Exception {ServerSocket ss = new ServerSocket(9999);while (true) {Socket socket = ss.accept();MyRunnable myRunnable = new MyRunnable(socket);list.add(socket);Thread t = new Thread(myRunnable);t.start();}}}
然后,服务端接收到客户端的消息后要调用客户端集合把消息发给每一个客户端(即用服务端得到的管道得到输出流,由客户端的输入流接收即可)
public class MyRunnable implements Runnable{private Socket socket;public MyRunnable(Socket socket){this.socket=socket;}@Overridepublic void run() {try {InputStream is=socket.getInputStream();BufferedReader sr=new BufferedReader(new InputStreamReader(is));String len;while ((len=sr.readLine())!=null){System.out.println(len);for (Socket socket1 : SocketService.list) {PrintWriter pw=new PrintWriter(new OutputStreamWriter(socket1.getOutputStream()));pw.println(len);pw.flush();}}} catch (Exception e) {SocketService.list.remove(socket);}}}
客户端就要调用新建的线程
public static void main(String[] args) throws Exception{Socket socket=new Socket(InetAddress.getLocalHost(),9999);Thread t=new Thread(new MyRunnableReceive(socket));t.start();OutputStream os=socket.getOutputStream();PrintStream ps=new PrintStream(os);Scanner sc=new Scanner(System.in);while (true) {System.out.println("请输入");String say=sc.next();if ("exit".equals(say)) {socket.close();break;}else {ps.println(say);}}}}
之前的客户端都是CS架构,客户端实需要我们自己开发实现的
而BS结构是浏览器访问服务端,不需要开发客户端
BS架构模拟
注意: 服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别
HTTP响应数据的协议格式:就是给浏览器显示的网页信息
格式如下
代码展示
public class ServeDemo {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(2222);while (true) {Socket socket=ss.accept();MyRunnable myRunnable=new MyRunnable(socket);Thread t=new Thread(myRunnable);t.start();}}}class MyRunnable implements Runnable{private Socket socket;public MyRunnable(Socket socket){this.socket=socket;}@Overridepublic void run() {try {PrintWriter pw=new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));pw.println("HTTP/1.1 200 OK");pw.println("Content-Type:text/html;charst=UTF-8");pw.println();//下面这行输入自己想写的内容,其他的照着写就行pw.println("hello");pw.close();} catch (Exception e) {e.printStackTrace();}}}
线程池优化模式
public class ServeDemo {public static void main(String[] args) throws Exception{ServerSocket ss=new ServerSocket(2222);ExecutorService pool=new ThreadPoolExecutor(3,5,2,TimeUnit.MINUTES,new ArrayBlockingQueue<>(5),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());while (true) {Socket socket=ss.accept();MyRunnable myRunnable=new MyRunnable(socket);pool.execute(myRunnable);}}}class MyRunnable implements Runnable{private Socket socket;public MyRunnable(Socket socket){this.socket=socket;}@Overridepublic void run() {try {PrintWriter pw=new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));pw.println("HTTP/1.1 200 OK");pw.println("Content-Type:text/html;charst=UTF-8");pw.println();pw.println("hello");pw.close();} catch (Exception e) {e.printStackTrace();}}}
小总结
TCP通信如何实现BS请求网页信息回来呢” />浏览器使用什么协议规则呢?
- HTTP协议 (简单了解下)
URL了解
当我们进入我们的博客主界面时,在地址栏处会出现一连串的英文字母,有人把它叫网址,但并不准确,其实这一连串就是URL
如下,是我的博客主页
一个完整的URL分为以下几个部分
①协议
②网址
- 网址部分包含域名
③文件地址
文件地址包括:
虚拟目录
文件名部分
锚
协议部分以 “//” 为分隔符
在internet中,我们可以使用多种协议,常用的有http和https两种协议
在地址栏中输入网址时,协议部分是不用输入的,浏览器会自动补上默认的HTTP协议
网址部分为 “//” 到第一个 “/” 的中间部分
- 我们经常输入 “www.baidu.com”(百度)等,这是一个网站独一无二的网络名字,我们去掉 “www.” 这个前缀,“baidu.com” 这一部分就是域名,最右边 “.com” 是顶级域名
常见顶级域名
顶级域名 | 表示 |
---|---|
.com | 商业机构 |
.org | 非盈利性组织 |
.gov | 政府机构 |
.edu | 教育及科研机构 |
表示国家的
顶级域名 | 表示 |
---|---|
.cn | 中国 |
.us | 美国 |
.jp | 日本 |
.cc | 科科斯群岛 (澳大利亚) |
有时候公司的下属分工司或则公司下设的其他产品网站会使用一个与域名类似的二级域名
比如:
百度是:“baidu.com”
而百度视频是:“video.baidu.com”
百度贴吧是:“tieba.baidu.com”
百度文库是:“wenku.baidu.com”
虚拟目录为从域名后第一个 “/” 到最后一个 “/” 为止
文件名部分为从最后一个 “/” 开始到 “#” 为止
锚:“#” 最后面就是锚部分
虚拟目录,文件,锚都不是URL的必须部分,虽然URL看起来复杂,但我们输入URL的时候,只要输入网址或域名就行了