网络编程可以让程序与网络上的其他设备中的程序进行数据交互
所以,我们学习网络编程的主要目的就是为了实现网络通信
网络通信基本模式
常见的通信模式有如下2种形式: Client-Server(Cs)、Browser/Server(Bs)
实现网络编程关键的三要素
IP地址: 设备在网络中的地址,是唯一的标识
端口:应用程序在设备中唯一的标识
协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
即我们要实现与某人通信,需要知道对方的IP地址,对方用于联系软件(应用程序)的端口号(不同电脑上的同一软件的端口号一般相同),再通过相关协议实现消息的传递,即实现了网络通信。
模型图如下
IP地址
IP (Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志
常见的IP分类为:IPV4和IPv6
IPV4:
IPV4由32位(4字节)组成,底层由四个字节组成,以点分十进制表示法来展现
例如:11000000 10101000 00000001 01000010——>192.168.1.66
(每一个字节是一组数,以“ . ”隔开)
IP地址形式:
公网地址 和 私有地址(局域网使用)
192.168.开头的就是常见的局域网地址,范用即为192.168.0.0–192.168.255.255,专门为组织机构内部使用
IP常用命令:
ipconfig:查看本机IP地址
ping IP地址:检查网络是否连通
特殊IP地址:
InetAddress 的使用
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.com
System.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.14
System.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));
}
端口号
端口类型
周知端口:0~1023,被预先定义的知名应用占用(如: HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户进程或某些应用程序。(如: Tomcat占用8080,MVSQL占用3306)
动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错
计算机网络中,连接和通信数据的规则被称为网络通信协议
网络通信协议有两套参考模型
OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广
TCP/IP参考模型(或TCP/IP协议): 事实上的国际标准
传输层的2个常见协议
TCP(Transmission Control Protocol) : 传输控制协议
UDP(User Datagram Protocol): 用户数据报协议
TCP协议:
使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
传输前,采用 “三次握手” 方式建立连接,所以是可靠的
在连接中可进行大数据量的传输
连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低
TCP协议通信场景
TCP三次握手确立联系模型图
TCP四次挥手断开连接模型图
UDP协议:
UDP是一种无连接、不可靠传输的协议
将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
每个数据包的大小限制在64KB内
发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
可以广播发送,发送数据结束时无需释放资源,开销小,速度快
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
①在左上方有这个标志,点击一下,再点第一个选项
UDP的接收端为什么可以接收很多发送端的消息?
UDP有三种通信方式
单播: 单台主机与单台主机之间的通信
广播:当前主机与所在网络中的所有主机通信
组播:当前主机与选定的一组主机的通信
(以上我们所说的都为单播,下面我们来说说广播和组播)
模型图如下
广播
UDP要实现广播,需使用广播地址: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是建立一个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;
}
@Override
public 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));
}
}
}
本次使用线程池的优势
服务端可以复用线程处理多个客户端,可以避免系统瘫痪
适合客户端通信时长较短的场景
即时通信是什么含义,要实现怎么样的设计?
即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
即时通信需要进行端口转发的设计思想
服务端需要把在线的Socket管道存储起来
一旦收到一个消息要推送给其他管道
大概模型如下的整个过程
那么,服务端除了接数据外也要往外输数据,客户端也要收数据,那么客户端的收数据也要写一个线程(也可以不写,但那样容易晕)
public class MyRunnableReceive implements Runnable{
private Socket socket;
public MyRunnableReceive(Socket socket){
this.socket=socket;
}
@Override
public 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;
}
@Override
public 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结构是浏览器访问服务端,不需要开发客户端
注意: 服务器必须给浏览器响应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;
}
@Override
public 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;
}
@Override
public 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请求网页信息回来呢?
客户端使用浏览器发起请求(不需要开发客户端)
服务端必须按照浏览器的协议规则响应数据
浏览器使用什么协议规则呢?
当我们进入我们的博客主界面时,在地址栏处会出现一连串的英文字母,有人把它叫网址,但并不准确,其实这一连串就是URL
如下,是我的博客主页
①协议
②网址
③文件地址
文件地址包括:
虚拟目录
文件名部分
锚
协议部分以 “//” 为分隔符
在internet中,我们可以使用多种协议,常用的有http和https两种协议
在地址栏中输入网址时,协议部分是不用输入的,浏览器会自动补上默认的HTTP协议
网址部分为 “//” 到第一个 “/” 的中间部分
常见顶级域名
顶级域名 | 表示 |
---|---|
.com | 商业机构 |
.org | 非盈利性组织 |
.gov | 政府机构 |
.edu | 教育及科研机构 |
表示国家的
顶级域名 | 表示 |
---|---|
.cn | 中国 |
.us | 美国 |
.jp | 日本 |
.cc | 科科斯群岛 (澳大利亚) |
有时候公司的下属分工司或则公司下设的其他产品网站会使用一个与域名类似的二级域名
比如:
百度是:“baidu.com”
而百度视频是:“video.baidu.com”
百度贴吧是:“tieba.baidu.com”
百度文库是:“wenku.baidu.com”
虚拟目录为从域名后第一个 “/” 到最后一个 “/” 为止
文件名部分为从最后一个 “/” 开始到 “#” 为止
锚:“#” 最后面就是锚部分
虚拟目录,文件,锚都不是URL的必须部分,虽然URL看起来复杂,但我们输入URL的时候,只要输入网址或域名就行了