文章目录
1、今日内容
-
什么网络编程
-
网络编程3要素(IP地址,端口号,协议)
-
使用UDP协议实现网络编程
-
使用TCP协议实现网络编程
-
文件的上传
2、什么是网络编程?
网络编程主要解决的问题是:多个或者同设备间的数据的通讯,主要研究的就是进程间的通讯。
要实现网络编程有3个必要条件:
(1)IP地址:用来表示设备在网络中的唯一性。
(2)端口号:用来标识某一个进程在计算机中的唯一性。
(3)协议:定义数据传输的规则。常见的协议有UDP和TCP.
3、IP地址
IP地址可以划分为IPV4和IPV6。
IPV4
我们都知道计算机底层存储的都是二进制数据,因此ip地址也是一个二进制数据。ipv4版的ip地址是一个32位(也就是4个字节)的二进制数,通常被分割为4部分,每一部分占8位。(由于每一部分只占用1个字节,因此每一部分的取值范围是:0~255)各个部分使用 “ . ” 进行分割。如下图所示:01100100.00000100.00000101.00000110
但是由于二进制数在日常生活中使用起来并不是特别的方便,因此,后期在使用的时候会把每一部分转换成对应的十进制。这种计数方式被称之点分十进制。
上面的ip地址如果使用点分十进制进行表示,那么就是:100.4.5.6 。即使,使用点分十进制的表示方式来表示一个ip地址,也并不是特别容易记忆。为了方便记忆,我们可以使用 “ 一连串用点分隔的字符 ” 来表示ip地址,比如: www.itcast.cn ;这一连串用点分隔的字符就是域名。
后期可以使用域名访问指定的服务器,在进行访问的时候,首先需要经过DNS(域名解析服务器)服务器进行域名解析,解析完毕以后就可以直接通过ip地址访问对应的服务器了。
Ipv6
现今的互联网络发展蓬勃,截至2018年1月,全球上网人数已达40.21亿,IPv4仅能提供约42.9亿个IP位置。随着互联网的发展,IPV4在某一个时间点就会枯竭,为了解决ip地址枯竭问题,ipv4的下一代版本ipv6就诞生了。IPv6具有比IPv4大得多的地址空间。这是因为IPv6采用128位的地址,而IPv4使用的是32位。因此ipv6版的ip地址支持2^128(3.4 * 10 ^ 38)个ip地址。
IPv6二进位制下为128位长度,以16位为一组,每组以冒号":“隔开,可以分为8组,每组以4位十六进制方式表示。例如:2001:0db8:85a3:08d3:1319:8a2e:0370:7344 是一个合法的IPv6地址。类似于IPv4的点分十进制,同样也存在点分十六进制的写法,将8组4位十六进制地址的冒号去除后,每位以点号”."分组,例如:2001:0db8:85a3:08d3:1319:8a2e:0370:7344 则也可以标记为:2.0.0.1.0.d.b.8.8.5.a.3.0.8.d.3.1.3.1.9.8.a.2.e.0.3.7.0.7.3.4.4
同时IPv6在某些条件下可以省略:
(1)每项数字前面的0可以省略,省略后前面数字仍是0则继续,例如下组IPv6是等价的。
(2)可以用双冒号"::"表示一组0或多组连续的0,但只能出现一次:
4、常见的DOS命令
(1)ipconfig 查看本机的ip地址
(2)ping 检查本机和其他计算机的网络连通性
(3) **执行ipconfig命令以后得到的结果:**Windows IP 配置
以太网适配器 以太网:
连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::e009:947a:77b9:b3da%10 // // 本机在局域网的ipv6的ip地址
IPv4 地址 . . . . . . . . . . . . : 192.168.20.73 // 本机在局域网的ipv4的ip地址
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.20.1
C:\Users\itcast_hly>
%10 : 表示的含义就是这个ipv6的ip地址在网卡标号为10的网卡上可以使用。
(4)查看网卡的编号:
① cmd
② netsh
③ interface ipv6
④ 使用show joins命令
(5)ping命令的使用格式: ping ip地址/域名
**(6)特殊的ip地址:**127.0.0.1被称之本地回环地址,表示的就是本机。提供本地回环地址的目的:进行测试。
5、InetAddress类的使用
作用: 用来表示IP地址(因为Java语言是面向对象的语言,任何东西都可以看做成对象,ip地址也不列外)
需要掌握的内容:如何获取InetAddress的实例
public static InetAddress getByName(String host) ; // host参数可以是主机名也可以是ip地址的字符串表现形式
注意:这个主机名称尽量不要使用中文,如果使用中文,后期在使用dubbo这个框架的时候很有可能会出现问题。
6、端口号
端口号:用来标识某一个进程在计算机中的唯一性。端口号的取值范围:0~65535 ,0~1023之间的端口号用于一些知名的网络服务和应用(一般都是系统服务和应用),用户的普通应用程序需要使用1024以上的端口号,从而避免端口号被另外一个应用或服务所占用。
注意: 一般情况下一个应用程序占用一个端口。
7、协议UDP和TCP的区别(重点)
(1)协议:就是数据传输的规则。常见的协议: UDP和TCP
(2)UDP协议和TCP协议的区别:
UDP协议的特点:
① 面向无连接
② 传输数据的速度是比较快,但是传输数据有大小限制,一次最多传输64K
③ 不安全的,数据有可能会丢失
- 常见的应用场景:对讲机,发短信
TCP协议的特点:
① 面向连接的协议
② 传输数据的速度是比较慢的,传输数据没有大小限制
③ 安全的,不会产生数据的丢失
- 常见的应用场景:打电话
8、基于UDP协议的网络编程
基于UDP协议的网络编程:在进行通信的时候,使用的都是DatagramSocket来实现通讯的。数据在进行传输的时候传输的都是数据包,在Java中给我们提供了一个类用来表示这个数据包:DatagramPacket
(1)发送端的代码实现
实现步骤:
1、 创建DatagramSocket对象(无参的构造方法)
2、 创建DatagramPacket对象(主要是用来封装我们需要传输的数据)
3、 调用DatagramSocket对象中方法来完成数据的发生(send方法)
4、 释放资源
代码的实现
public class Send {
public static void main(String[] args) throws IOException {
//1.找码头(无参的构造方法)
DatagramSocket ds = new DatagramSocket();
//2.打包礼物(数据数组,数组长度,主机地址,主机端口)
String str = "Hello接收端";
byte[] bytes = str.getBytes();
InetAddress byName = InetAddress.getByName("127.0.0.1");
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,byName,10000);
//3.由码头发送包裹
ds.send(dp);
//4.付钱走羊
ds.close();
}
}
(2)接收端的代码实现
实现步骤:
1、 创建DatagramSocket对象(有参构造方法)
2、 创建一个DatagramPacket对象(接收发生端所传递过来的数据包)
3、 调用DatagramSocket对象中的方法(receive)接收数据
4、 解析数据包(就是从DatagramPacket对象中获取数据)
5、 释放资源
代码的实现:
public class Receive {
//注意点:
//1.要先运行接收端,再运行发送端
//2.如果接收端再启动之后,没有接收到数据,那么会死等(阻塞).
//3.在接收数据的时候,需要调用一个getLength方法,表示接收到了多少字节
public static void main(String[] args) throws IOException {
//1.找码头 --表示接收端从10000端口接收数据的
DatagramSocket ds = new DatagramSocket(10000);
//2.创建一个新箱子
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
//3.接受礼物,把礼物放到新的箱子中
ds.receive(dp);
//4.从新的箱子里面获取礼物
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data,0,length));
//5.拿完走羊
ds.close();
}
}
9、组播的实现(了解)
概述: 发送者发送的数据可以被小组内的所有接收者所接收。组播的接收范围介于单播和广播之间。
组播ip地址:224.0.0.0~239.255.255.255
要想实现组播,我们需要使用DatagramSocket
的子类: MulticastSocket
(1)发送端的代码实现:
public class send {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
String s = "hello 组播";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.1.0");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
ds.send(dp);
ds.close();
}
}
(2)接收端的代码实现:
public class receive {
public static void main(String[] args) throws IOException {
MulticastSocket ms = new MulticastSocket(10000);
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
//把当前计算机绑定一个组播地址,表示添加到这一组中.
ms.joinGroup(InetAddress.getByName("224.0.1.0"));
ms.receive(dp);
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data,0,length));
ms.close();
}
}
10、广播的实现
要实现广播,在进行数据发送的时候就需要指定广播地址:255.255.255.255
具体的代码实现:
1、 发送端
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
String s = "广播 hello";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("255.255.255.255");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
ds.send(dp);
ds.close();
}
(2)接收端
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10000);
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
ds.receive(dp);
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data,0,length));
ds.close();
}
11、基于TCP协议实现网络编程
基于TCP协议实现网络编程思想:TCP协议在进行通讯的时候首先需要建立连接,就需要保证接收端(服务器端)首先启动起来。连接建立以后再进行输出传输的时候使用的io流实现数据的传输。要完成进程间的通讯,就需要创建两端(发送端和接收端),Java针对不同的两端提供了不同的类:
① 发送端所对应的类(Socket)
② 接收端所对应的类(ServerSocket)
(1)发送端的代码实现步骤:
① 创建Socket对象
② 获取输出流对象
③ 写数据
④ 释放资源
代码实现如下:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",10000);
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
os.close();
socket.close();
}
}
(2)接收端的代码实现步骤:
① 创建ServerSocket对象
② 监听客户端(阻塞式的)
③ 获取输入流对象
④ 读取数据
⑤ 释放资源
代码的实现:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket accept = ss.accept();
InputStream is = accept.getInputStream();
int b;
while((b=is.read())!=-1){
System.out.print((char)b);
}
is.close();
ss.close();
}
}
12、TCP通讯的原理
(1)原理
① 服务端调用accept这个方法的时候是一个阻塞式。
② 客户端和服务端进行连接的时候需要经过3次握手,客户端和服务端断开连接的时候需要经过4次挥手。
③ read方法是一个阻塞式的方法,阻塞的前提:就是通道中没有可读数据了就阻塞了。当客户端和服务端连接断开以后阻塞结束。
(2)三次握手
3次握手的过程大致如下:
① 客户端到服务端: 我要连接
② 服务端到客户端: 好的,已经连接上了
③ 客户端到服务端: 收到,确认已连接上了
从底层数据传输的角度而言,总共进行3次数据的传输:
① 从客户端到服务端
② 从服务端到客户端
③ 从客户端到服务端
要想获取这些数据的传输过程,可以使用一些常见的抓包工具进行数据的抓取:wireshark
客户端代码
// 创建Socket对象
// 169.254.170.6这个地址是安装wireshark的时候产生的本地回环地址
Socket socket = new Socket("169.254.170.6" , 9999) ;
// 让线程休眠60s
TimeUnit.SECONDS.sleep(60);
服务端代码
// 创建ServerSocket对象
ServerSocket serverSocket = new ServerSocket(9999) ;
// 让线程休眠60s
TimeUnit.SECONDS.sleep(60);
首先运行服务端,然后在运行客户端,在wireshark工具中捕获完整的通信过程,结果如下图所示:
①第一次握手
在第一次“握手"时,**客户端向服务端发送SYN标志位,目的是与服务端建立连接。Seq代表sequence number(发送数据流序号), Seq**的值是0,代表客户端第一次给服务端发送数据。另外Len=0也可以看出来是没有数据可供发送的。客户端仅仅发送一个SYN标志位到服端代表要进行连接。
说明:Seq的值是5,说明在数据流中曾经一共发送了 1, 2, 3,4 这4次数据。而在本次"握手"中,
② 第二次握手
第二次"握手"时,服务端向客户端发送 SYN ACK 标志位,其中ACK标志位表示是对收到的数据包的确认,说明服务端接收到了客户端的连接。**ACK**的值是1,表示服务端期待下一次从客户端发送数据流的序列号是1,而Seq=0代表服务端曾经并没有给客户端发送数据,而本次也没有发送数据,因为Len=0也证明了这一点。
③ 第三次握手
第三次“握手”时,客户端向服务端发送的ACK标志位为1, Seq的值是1。Seq=l代表这正是服务端所期望的Ack=1。Len=0说明客户端这次还是没有向服务端传递数据,而客户端向服务端发送ACK 标志位为1的信息,说明客户端期待服务端下一次传送的Seq的值是1。
(3)四次挥手
客户端与服务端在断开连接的时候需要进行4次"挥手",4次"挥手"的过程如下:
① 客户端到服务端:我关了
② 服务端到客户端:好的,收到
③ 服务端到客户端:我也关了
④ 客户端到服务端:好的,收到
抓包图示
首先运行服务端,然后在运行客户端,在wireshark工具中捕获完整的通信过程,结果如下图所示:
第一次"挥手"如下图所示
在第一次"挥手"时,客户端到服务器发送标志位FIN ACK,告知服务端客户端关闭了。Seq=1表示本次数据流的序号为1,Ack=1表示客户端期望服务端下一次发送的数据流的序号为1。len=0,说明没有数据传输到服务端。
第二次"挥手"如下图所示
在第二次"挥手"时,服务端向客户端发送标志位ACK,Seq=1代表的正是客户端想看的Ack=1。Ack=2表示服务端期望下一次客户端发送的数据流的序号为2。len=0,说明没有数据传输到客户端。
第三次"挥手"如下图所示
在第三次"挥手"时,服务端向客户端发送标志位FIN ACK,告知客户端服务端关闭了。Seq=1代表的正是客户端想看的Ack=1。Ack=2表示服务端期望下一次客户端发送的数据流的序号为2。len=0,说明没有数据传输到客户端。
第四次"挥手"如下图所示
在第四次"挥手"时,客户端向服务端发送标志位ACK,告知服务端客户端已经收到服务端关闭信息。Seq=2代表的正是服务端想看的Ack=2,ACK=2表示客户端期望下一次服务端发送的数据流的序号为2。
连接断开的过程总结:
13、TCP的练习
需求:客户端向服务端发送数据,服务端接收到数据以后给客户端进行反馈,客户端去读取反馈在控制台进行输出。
客户端的代码:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",10000);
//建立网络输入流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("hello你好");
bw.flush();
//bos.close();如果在这里关流,会导致整个socket都无法使用
socket.shutdownOutput();//给服务器一个结束标记,告诉服务器文件已经传输完毕
//建立网络输出流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String str;
while ((str=br.readLine())!=null){
System.out.println(str);
}
br.close();
socket.close();
}
}
服务器端的代码:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket accept = ss.accept();
//建立网络输出流
BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
String str;
while ((str=br.readLine())!=null){
System.out.println(str);
}
//br.close();//如果在这里关流,会导致整个socket都无法使用
//建立网络输入流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("我已收到!");
bw.flush();
bw.close();
accept.close();
ss.close();
}
}
14、TCP的练习2
文件上传的原理:

代码1
客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10000);
//是本地的流,用来读取本地文件的.
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D://111.png"));
//建立网络输出流(字节)
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
int b;
while((b=bis.read())!=-1){
bos.write(b);
}
bos.flush();
//bos.close();//如果在这里关流,会导致整个socket都无法使用
//给服务器一个结束标记,告诉服务器文件已经传输完毕
socket.shutdownOutput();
//建立网络输入流(字符流)。作用读取服务器响应的数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String str;
while((str=br.readLine())!=null){
System.out.println(str);
}
}
}
服务器
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket accept = ss.accept();
//获取网络中的输入流
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//获取本地输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("I:\\heima_idea1\\project01\\day17Socket\\Source\\"+UUID.randomUUID()+".png"));
int b;
while((b=bis.read())!=-1){
bos.write(b);
}
//获取网略输出流(字符流)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("上传成功");
bw.flush();
bw.close();
bos.close();
bis.close();
accept.close();
ss.close();
}
}
代码2
改进:服务器端添加while(true){可以接收多个服务器上传文件}
改进:服务器保存文件,文件名修改为UUID,不会出现文件覆盖。
服务器
/**
* 改进:服务器端添加while(true){可以接收多个服务器上传文件}
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
while (true){
Socket accept = ss.accept();
//获取网络中的输入流
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//获取本地输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("I:\\heima_idea1\\project01\\day17Socket\\Source\\"+UUID.randomUUID()+".png"));
int b;
while((b=bis.read())!=-1){
bos.write(b);
}
//获取网略输出流(字符流)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("上传成功");
bw.flush();
bw.close();
bis.close();
bos.close();
accept.close();
ss.close();
}
}
}
代码3
线程类
public class ThreadSocket implements Runnable{
private Socket acceptSocket;
public ThreadSocket(Socket acceptSocket) {
this.acceptSocket = acceptSocket;
}
@Override
public void run() {
BufferedInputStream bis=null;
BufferedOutputStream bos=null;
BufferedReader br = null;
try {
//获取网络中的输入流
bis = new BufferedInputStream(acceptSocket.getInputStream());
//获取本地输出流
bos = new BufferedOutputStream(new FileOutputStream("I:\\heima_idea1\\project01\\day17Socket\\Source\\"+ UUID.randomUUID()+".png"));
int b;
while((b=bis.read())!=-1){
bos.write(b);
}
//获取网略输出流(字符流)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(acceptSocket.getOutputStream()));
bw.write("上传成功");
bw.flush();
bw.close();
}catch (Exception e){
e.printStackTrace();
} finally {
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (acceptSocket != null){
try {
acceptSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务器
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
while (true){
Socket accept = ss.accept();
ThreadSocket ts = new ThreadSocket(accept);
new Thread(ts).start();
}
}
}
代码4
改进:添加线程池。
服务器端
/**
* 改进:添加线程池
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
10, //线程池的总数量
60, //临时线程空闲时间
TimeUnit.SECONDS, //临时线程空闲时间的单位
new ArrayBlockingQueue<>(5),//阻塞队列
Executors.defaultThreadFactory(),//创建线程的方式
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
while (true){
Socket accept = ss.accept();
ThreadSocket ts = new ThreadSocket(accept);
pool.submit(ts);
}
}
}
15、服务端代码问题分析
(1)服务器只能监听一个客户端,没有办法监听多个客户端
解决方案:就是使用循环进行改进
(2) 文件名称重复问题
解决该问题的思想:保证文件名称唯一即可
如何保证唯一:可以使用JDK所提供的UUID这个类中的public static UUID randomUUID()
(3) 服务器的效率太低
解决方案:针对每一个客户端需要去开启一个线程
虽然使用多线程可以提高服务器端的效率,但是针对每一个客户端都开启一个线程也会造成资源的浪费。为了解决服务器的资源可以线程池进行改进。