Java网络编程
计算机网络就是通过传输介质、通信设施和网络协议,把分散在不同地点的计算设备互连起来,实现资源共享和数据传输的系统
TCP/IP协议簇
TCP/IP协议栈是一系列网络协议的总和,是构成网络通信的核心骨架
分层模型
TCP/IP协议栈的分层模型常见的有2个,分别是TCP/IP参考模型和ISO组织提出的OSI参考模型。
在TCP/IP参考模型中将网络分为网络访问层【数据链路层】、互联网层【网络层】、传输层、应用层共4层
OSI参考模型分为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层共7个层。OSI参考模型是一个开放的通信系统互联参考模型
TCP/IP参考模型
TCP/IP协议采用4层架构,从上向下分为应用层、传输层、网络层、链路层,每一层相关协议都依次对数据包进行处理,并携带相应的首部,最终在链路层生成以太网数据包,通过物理介质进行传输,传送到对方主机后,对方主机再依次从下向上使用相应协议进行拆包,最终经应用层数据交给应用程序进行处理
配送车就是物理介质(网卡,网线等),配送站就是网关,快递员就是路由器,收货地址就是IP地址,联系电话就是MAC地址
三次握手
TCP是面向连接的协议,建立连接需要有三个阶段:连接建立、数据传送和连接释放。其中连接建立需要经历三个步骤,通常称为三次握手
- 第一次握手,客户端发送请求
- 第二次握手,服务器端回传确认
- 第三次握手,客户端回传确认
类比日常打电话:
A:喂,你听得到吗?
B:可以,你听得到我说话吗?
A:可以。我们聊天吧!
过程:
- A主动打开连接,B被动打开连接。一开始B的TCP服务器进程创建传输控制块TCB,处于LISTEN(收听)状态,等待用户进程的连接请求。A的TCP客户进程也是首先创建传输控制模块TCB。向B发送连接请求报文段,首部的同步位SYN=1,同时选择一个初始序号seq=x。TCP规定SYN报文段(即SYN=1的报文段)不能携带数据,但要消耗一个序号。这时TCP客户进程进入SYN-SENT(同步已发送)状态。
- B收到连接请求报文段后,如果同意建立连接,则向A发送确认。在确认报文段中应把SYN位和ACK位都置1,确认号是ack=x+1,同时为自己选择一个初始序号seq=y。这个报文段也不能携带数据,同样消耗一个序号。此时TCP服务器进程进入SYN-RCVD(同步收到)状态。
- TCP客户进程收到B的确认后,还要向B给出确认。确认报文段的ACK置1,确认号ack=y+1,而自己的序号seq=x+1。TCP标准规定,ACK报文可以携带数据,但如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是seq=x+1。此时TCP已建立连接,A进入ESTABLISHED(已建立连接)状态。当B收到A的确认后,也进入到ESTABLISHED状态。
四次挥手
由于TCP连接是双工的,所以每个方向都必须单独进行关闭
过程:
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2??MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
【面试】为什么连接的时候是3次握手,而断开连接时是4次挥手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【面试】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁
粘包
多个数据包存储在缓存中,对数据包的处理由于无法确认边界,所以经常采用估测值大小进行数据的读写,如果发送和接收数据的双方size不一致时,会使用发送方发送的若干个包数据到接收方时粘成一个包
原因
既可以时发送方造成的,也可能是接收方造成。粘包并不是TCP协议造成的,出现是因为应用层设计的缺陷
Nagle算法通过减少数据报数量的方式提供TCP传输性能
解决方案
应用层协议自己划分消息边界,常见的方案有基于长度或者基于终结符号
拥塞控制
防止过多的数据注入网络,以避免使网络中的路由器或者链路过载
拥塞控制前提是网络能够承受现有的网络负荷
不同于流量控制,流量控制就是抑制发送数据的效率,以便使接收方能够来得及接收数据
拥塞控制的机制
慢开始、拥塞避免、快重传和快恢复
- 慢开始就是当主机发送数据时,先进行探测,可以由小到大逐渐增加发送窗口
- 拥塞避免是让拥塞窗口缓慢增大,不是加倍,而是加1
- 不使用快重传就是当发送方并没有在规定的时间内收到确认信息,则拥塞窗口减少到1,并执行慢开始算法。快重传要求接收方每收到一个乱序的报文后立即确认
- 和快重传机制一起使用的是快恢复,将拥塞窗口的大小设置为慢开始的上限值的一半
IP地址
在网络中定位一个机器需要通过IP地址,IP协议可以分为IPv4和IPv6,IPv4采用的是点分十进制的计法,例如192.168.1.8
Java中提供了一个InetAddress实现对IP地址的封装,子类Inet4Address和Inet6Address,这个类一般会和Socket一起使用
InetAddress没有公共的构造方法,必须通过使用静态方法获取对应的实例
baidu.com:域名
www.baidu.com:主机名
InetAddress ia = InetAddress.getByName("www.baidu.com");
System.out.println(ia);
System.out.println(ia.getHostName());
System.out.println(ia.getHostAddress());
InetAddress ia2 = InetAddress.getLocalHost();
System.out.println(ia2);
InetAddress ia3 = InetAddress.getByName("192.168.56.100");
boolean bb = ia.isReachable(2000);
System.out.println(bb);
URL编程
java.net.URL对象用于代表一个网络环境的资源,资源可以是简单的文件或者目录,也可以是复杂对象的引用,例如数据库或者搜索引擎的查询。URL使用协议名、主机名、端口号和资源组成,基本格式是protocol://host:port/resource,例如http://www.yan.com:80,由于不同的协议有对应的标准端口号,如果使用标准端口,这个端口号可以省略,http协议的标准端口号为80
- URL统一资源定位器,实际上就是一个资源的指针
- URL统一资源标识符,实际上就是一个URL的名称
- 目前考虑到http协议缺少安全机制,很容易被监听,所以引入https协议。https=http+SSL安全套接层,可以实现传输数据的加密 ,默认端口号443
try {
URL url = new URL("http://campus.51job.com/ssjkq/images/banner.jpg");
InputStream is = url.openStream();
OutputStream os = new FileOutputStream("d:/banner.jpg");
byte[] buffer = new byte[8192];
int len = 0;
while ((len=is.read(buffer))>0) {
os.write(buffer,0,len);
}
is.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
可以通过URL对象获取访问相关的属性
- String getFile()获取资源名
- String getHost()获取主机名
- String getPath()获取路径部分的名称
- int getPort()获取端口号如果不能获取,则返回-1
URL url = new URL("http://campus.51job.com:80/ssjkq/images/banner.jpg"); System.out.println(url.getFile());
System.out.println(url.getHost());
campus.51job.com System.out.println(url.getPath());
System.out.println(url.getPort());
可以使用字符串解析获取相应部分的内容
String ss = "http://campus.51job.com:80/ssjkq/images/banner.jpg";
String fileName = ss.substring(ss.lastIndexOf("/") + 1);
System.out.println(fileName);
int pos1 = ss.indexOf("http://") + "http://".length();
int pos2 = ss.indexOf("/", pos1);
String hostName = ss.substring(pos1, pos2);
System.out.println(hostName);
String pathName = ss.substring(pos2);
System.out.println(pathName);
int pos3 = ss.lastIndexOf(":");
if (pos3 != -1) {
int pos4 = ss.indexOf("/", pos3);
String port = ss.substring(pos3 + 1, pos4);
System.out.println(port);
}
重要方法
- openConnection():URLConnection可以获取输入、输出流
http协议依靠的是全双工的TCP协议
- openStream():InputStream直接获取服务器的响应输出流
URL url = new URL("https://news.cctv.com/2022/04/10/ARTIWLj3f0W2lIKwP8lp7HTb220410.shtml") ;
InputStream is = url.openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("d:/bb.html")));
String tmp="";
while((tmp=br.readLine())!=null){
System.out.println(tmp);
pw.println(tmp);
}
br.close();
pw.close();
URL vs URLConnection
从语义的角度上来说:URL代表一个资源的位置,URLConnection代表的是连接
Java中提供了两种读取数据的方法:
1、通过URL对象直接获取相关的网络信息。2、先获取一个URLConnection实例,然后再得到响应的InputStream和OutputStream,实现数据的读写
URL是一种简单直接的方法,但是缺乏灵活性,并且只能读取只读性质的信息;URLConnection提供了非常灵活有效的方法来读取网络资源
URL url = new URL("https://news.cctv.com/2022/04/10/ARTIWLj3f0W2lIKwP8lp7HTb220410.shtml") ;
URLConnection connection = url.openConnection();
InputStream is = connection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("d:/bb.html")));
String tmp = "";
while ((tmp = br.readLine()) != null) {
System.out.println(tmp);
pw.println(tmp);
}
br.close();
pw.close();
TCP编程
TCP是一种面向虚电路连接的端对端的保证可靠传输的协议,使用TCP协议可以得到一个顺序的无差错的数据流
UDP是一种不保证数据的可靠性,但是协议简单、传输速度快。一般用于视频或者音频的传输,不需要很高的可靠性,可以容忍偶尔的丢帧
在具体编程中发送方和接收方必须成对的使用socket建立连接,在tcp协议的基础上进行通信
Socket
socket套接字就是两个进行通信的主机之间逻辑连接的端点。socket编程实现主要设计到客户端和服务端两方面。首先在服务器端创建一个服务器套接字ServerSocket,并将其附加到一个端口(逻辑编号)上,服务器可以通过这个端口监听客户端的连接请求。端口号是int类型,取值范围为0-65535,但是一般0-1024属于特殊保留的端口。
服务器和客户端建立连接时,需要服务器的域名或者对应的IP地址,加上端口号,可以打开一个套接字。当服务器接收到客户端的连接请求后,服务器和客户端之间的通信实际上就是一种输入输出流的操作
典型的网络编程模型
ServerSocket类
java.net.ServerSocket用于表示一个服务器端套接字,主要功能是监听客户端的连接请求,并按照客户端的连接请求存入到请求队列中,默认请求队列大小为50
- ServerSocket()创建非绑定服务器指定端口的套接字
- ServerSocket(int)创建绑定到服务器指定接口的套接字,其中int类型参数就是对应的监听端口号,取值范围为0-65535,一般不建议使用1024以下的端口号,其中0表示使用任意的非占用的端口,如果当前指定端口已经被占用,则出异常
for(int i=0;i<=65535;i++){
ServerSocket ss = null;
try{
ss=new SErverSocket(i);
}catch(Exception e){
System.out.println(i+"端口被占用");
}finally{
if(ss!=null)
ss.close();
}
}
Socket
客户端通过构建Socket对象实现连接请求
- Socket(InetAddress,int),InetAddress就是需要连接的服务器,int就是服务器的监听端口号
- Socket(String,int) String就是服务器的名称或者IP地址。如果构建Socket对象成功,则连接创建,否则ConnectException
服务器编程
- ServerSocket ss = new ServerSocket(9999);//监听端口,不是具体的连接端口
- Socket socket=ss.accept();//阻塞服务器线程,等待客户端的连接请求
- InputStream is = socket.getInputStream();
- OutputStream os = socket.getOutputStream();
InputStreamReader OutputStreamWriter
客户端编程
- Socket socket=new Socket(“服务器的地址”,int 端口号);
- InputStream is = socket.getInputStream();
- OutputStream os = socket.getOutputStream();
- 关闭close
简单地C/S编程实现
要求:客户端发送hello信息,服务器端接收到信息后,添加一个日期信息,然后回传给客户端,客户端在控制台上打印显示
服务器端程序
ServerSocket ss = new ServerSocket(9000);
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String str =br.readLine();
System.out.println("服务器接收到客户端的数据:"+str);
PrintStream ps = new PrintStream(os);
DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String sdate=df.format(new Date());
str+=("[server:]"+sdate);
ps.println(str);
ps.flush();
ps.close();
br.close();
socket.close();
具体的编码流程:
- 创建ServerSocket对象,绑定监听端口
- 通过accept方法阻塞程序执行,并等待监听客户端的连接请求
- 连接建立后,通过Socket对象获取输入输出流
- 通过输入输出流读取客户端发送的数据或者向客户端发送数据
- 关闭相应的资源
客户端程序
注意:参数2服务器的监听端口号是客户端向服务器的监听端口发起连接请求,但真正连接所使用的端口号并不是这个监听端口号
Socket socket=new Socket("localhost",9000);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("Hello Server");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String str = br.readLine();
System.out.println("客户端接收到服务器的响应信息"+str);
ps.close();
br.close();
socket.close();
具体的编码流程:
- 创建Socket对象,指明需要连接的服务器地址和对应的监听端口号
- 连接建立后,通过Socket对象获取输入输出流
- 通过输入输出流读取服务器端发送的数据或者向服务器端发送数据
- 关闭相应的资源
服务器端编程
主线程一直处于阻塞等待状态,一旦连接成功则启动一个线程对外提供服务。主线程并不处理响应逻辑,相应处理是由子线程负责
ServerSocket ss=new ServerSocket(9000);
while(true){
Socket socket = ss.accept();
new Thread(new MyRunnable(socket)).start();
}
启动工作线程时主线程会将创建好的Socket对象传递过去,在工作线程的run方法中直接使用,并执行对应的处理逻辑
public class MyRunnable implements Runnable {
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader br = null;
PrintStream ps = null;
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
br = new BufferedReader(new InputStreamReader(is));
ps = new PrintStream(os);
String str = br.readLine();
System.out.println(Thread.currentThread() + ":" + str);
Date now = new Date();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String sdate = df.format(now);
ps.println(sdate);
ps.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null)
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
if (ps != null)
ps.close();
if (socket != null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
系统结构
客户机/服务器结构,简称C/S,胖客户端应用,是软件系统体系结构中的一种,基本上用于企业内部网络的应用系统,大部分的应用逻辑都集中在客户端中,而服务器一般只提供数据的存储支持
浏览器/服务器结构,简称B/S,瘦客户端应用,是目前主流的软件系统体系结构,主要逻辑集中在服务器端,客户端只负责一些简单的显示逻辑
HTTP协议
http协议
http超文本传输协议,建立在tcp协议之上的,属于应用层协议,是一种不保持连接的请求/响应式的协议。主要用于浏览器和web服务器之间的通信标准协议。目前有两个版本,1.0和1.1.1。1.0中采用的是非持续连接,1.1中采用的是持续连接,支持用户端和服务器持续的在这个连接上传送http报文
-
浏览器首先通过DNS域名服务将主机名转换为IP地址 -
默认情况下,客户端与打开80监听端口的服务器先建立一个TCP连接,URL还可以指定其他端口 -
客户端进行服务器发送消息,指定请求的资源,这个资源一般包括一个头部、可选,后面还有一个空行,最后是这个请求传送的数据 -
服务器处理请求后,向客户端回传响应信息。响应是以响应码开头,后面就是包含数据的响应头、一个空行以及所请求的文档信息或者错误信息 -
服务器关闭连接
1个TCP连接除非主动关闭,可以供http协议多次使用;默认情况下chrome针对一个网站可以建立6个TCP连接
http VS https
https就是在http协议的基础上引入SSL安全套接层,建立以数据传输安全为目标的http通道,针对传送的数据进行加密。因为http这个协议是以明文的方式发送数据,不提供任何方式的数据加密,所以不适合传输敏感数据。SSL依靠证书验证服务器的身份,对传送数据进行加密解密处理
- https协议需要申请证书,一般免费的证书较少,需要缴费
- http信息是明文传输,https采用的是ssl加密数据传输
- http协议使用80端口,https使用443端口
- http连接是无状态的,https是具有ssl+http协议构建的可以进行加密传输、身份认证的网络协议,比http协议安全
UDP编程
UDP是用户数据报协议的简称,是一种无连接的协议,每个数据报都是一个独立信息,包括完整的源地址和目标地址,它能够在网络上以任何可能的路径传送到目的地,因此是否能够到达目的地、到达目的地的时间以及内容的正确性都是不能保证的
UDP通信过程
发送数据报的过程:
- 使用DatagramSocket创建一个数据报套接字
- 使用DatagramPacket(byte[] data数据,int offset偏移量指定下标,int length长度,InetAddress接收方地址,port接收方端口号)创建一个要发送的数据报
- 使用DatagramSocket的send方法就可以发送数据报
- DatagramSocket socket=new DatagramSocket();
- DatagramPacket packet=new DataframPacket(byte[]具体的报文信息,int发送信息的长度,InetAddress接收方地址,int接收方的端口号)
- socket.send(packet);
- socket.close();
DatagramSocket socket=new DatagramSocket();
String str = "小胖";
byte[] data=str.getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length,InetAddress.getLocalHost(),9999);
socket.send(dp);
socket.close();
接收数据报的过程:
- 使用DatagramSocket创建一个数据报套接字,绑定到指定端口上
- 使用DatagramPacket(byte[]数据,int 长度)创建一个字节数组用于接收数据报
- 使用DatagramSocket的receive方法接收UDP数据报
- DatagramSocket socket=new DatagramSocket(9999);
- 声明一个空字节数组,用于存储接收到的数据,注意长度应该足够,否则有数据丢失
- DatagramPacket packet=new DatagramPacket(空数组,最大长度);
- socket.receive(packet);
- packet.getAddress()/packet,getPort();
- socket.close();
DatagramSocket socket=new DatagramSocket(9999);
byte[] buffer = new byte[30];
DatagramPacket dp=new DatagramPacket(buffer,buffer.length);
socket.receive(dp);
String res=new String(dp.getData(),0,dp.getLength());
System.out.println(dp.getData()==buffer);
System.out.println(buffer);
InetAddress ia=dp.getAddress();
int port=dp.getPort();
System.out.println(ia+"--"+port);
socket.close();
System.out.println("接收到的数据:"+res);
发送-接收-回传
客户端询问服务器时间,服务器回传具体当前时
DatagramSocket socket=new DatagramSocket();
String str="几点了?";
byte[] buffer=str.getBytes();
DatagramPacket packet=new DatagramPacket(buffer, buffer.length,InetAddress.getLocalHost(),9999);
socket.send(packet);
byte[] buffer2=new byte[8192];
packet=new DatagramPacket(buffer2, buffer2.length);
socket.receive(packet);
str=new String(packet.getData(),0,packet.getLength());
System.out.println("服务器时间为:"+str);
socket.close();
服务器需要满足第一次请求信息接收,在具体的UDP编程中实际上是没有服务器和客户端之分的
DatagramSocket socket = new DatagramSocket(9999);
byte[] buffer=new byte[8192];
DatagramPacket packet=new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String str = new String(packet.getData(),0,packet.getLength());
System.out.println("客户端请求问:"+str);
if ("几点了?".equals(str)) {
DateFormat df = new SimpleDateFormat("yyyy年M月d日H点m分s秒");
str=df.format(new Date());
}
InetAddress sender = packet.getAddress();
int port=packet.getPort();
byte[] data=str.getBytes();
packet=new DatagramPacket(data, data.length,sender,port);
socket.send(packet);
socket.close();
|