一、网络编程基础知识
计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。
对某个特定的计算机网络来说,它可能使用网络协议ABC,而另一个计算机网络可能使用网络协议XYZ。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用TCP/IP协议。
TCP/IP协议泛指互联网协议,其中最重要的两个协议是TCP协议和IP协议。只有使用TCP/IP协议的计算机才能够联入互联网,使用其他网络协议(例如NetBIOS、AppleTalk协议等)是无法联入互联网的。
1.IP地址
在互联网中,一个IP地址用于唯一标识一个网络接口(Network Interface)。一台联入互联网的计算机肯定有一个IP地址,但也可能有多个IP地址。
IP地址分为IPv4和IPv6两种。IPv4采用32位地址,类似101.202.99.12
,而IPv6采用128位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428
。IPv4地址总共有232个(大约42亿),而IPv6地址则总共有2128个(大约340万亿亿亿亿),IPv4的地址目前已耗尽,而IPv6的地址是根本用不完的。
IP地址又分为公网IP地址和内网IP地址。公网IP地址可以直接被访问,内网IP地址只能在内网访问。内网IP地址类似于:
- 192.168.x.x
- 10.x.x.x
有一个特殊的IP地址,称之为本机地址,它总是127.0.0.1
。
IPv4地址实际上是一个32位整数。例如:
106717964 = 0x65ca630c
= 65 ca 63 0c
= 101.202.99.12
如果一台计算机只有一个网卡,并且接入了网络,那么,它有一个本机地址127.0.0.1
,还有一个IP地址,例如101.202.99.12
,可以通过这个IP地址接入网络。
如果一台计算机有两块网卡,那么除了本机地址,它可以有两个IP地址,可以分别接入两个网络。通常连接两个网络的设备是路由器或者交换机,它至少有两个IP地址,分别接入不同的网络,让网络之间连接起来。
如果两台计算机位于同一个网络,那么他们之间可以直接通信,因为他们的IP地址前段是相同的,也就是网络号是相同的。网络号是IP地址通过子网掩码过滤后得到的。例如:
某台计算机的IP是101.202.99.2
,子网掩码是255.255.255.0
,那么计算该计算机的网络号是:
IP = 101.202.99.2
Mask = 255.255.255.0
Network = IP & Mask = 101.202.99.0
每台计算机都需要正确配置IP地址和子网掩码,根据这两个就可以计算网络号,如果两台计算机计算出的网络号相同,说明两台计算机在同一个网络,可以直接通信。如果两台计算机计算出的网络号不同,那么两台计算机不在同一个网络,不能直接通信,它们之间必须通过路由器或者交换机这样的网络设备间接通信,我们把这种设备称为网关。
网关的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫路由。
所以,一台计算机的一个网卡会有3个关键配置:
- IP地址,例如:
10.0.2.15
- 子网掩码,例如:
255.255.255.0
-
2.域名
因为直接记忆IP地址非常困难,所以我们通常使用域名访问某个特定的服务。域名解析服务器DNS负责把域名翻译成对应的IP,客户端再根据IP地址访问服务器。
用nslookup
可以查看域名对应的IP地址:$ nslookup www.liaoxuefeng.com
Server: xxx.xxx.xxx.xxx
Address: xxx.xxx.xxx.xxx#53
Non-authoritative answer:
Name: www.liaoxuefeng.com
Address: 47.98.33.223
有一个特殊的本机域名
localhost
,它对应的IP地址总是本机地址127.0.0.1
。3.网络模型
由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect 开放式系统互联)网络模型是ISO组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:
应用层,提供应用程序之间的通信;
- 表示层:处理数据格式,加解密等等;
- 会话层:负责建立和维护会话;
- 传输层:负责提供端到端的可靠传输;
- 网络层:负责根据目标地址选择路由来传输数据;
- 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。
互联网实际使用的TCP/IP模型并不是对应到OSI的7层模型,而是大致对应OSI的5层模型:
- TCP(Transmission Control Protocol)传输控制协议
传输基本单位是报文段
应用范围:运输层,进程之间
- UDP(User Datagram Protocol)用户数据报协议
传输基本单位是用户数据报
应用范围:运输层,不同主机上的进程之间
- IP(Internet Protocol)协议
用于网络层,主要为不同的主机之间的数据传输提供服务
服务与协议之间的关系
协议是对等的,服务是垂直的。
协议的作用是保证不同实体之间的传输是对等的,而不同层之间,通过“服务”来使下层给上层暴露出有限的接口,即层间接口,使得上层能够使用下层的服务,来完成协议的功能
参考:https://www.cnblogs.com/bqwzx/p/11053778.html
4.常用协议
IP协议是一个分组交换,它不保证可靠传输。而TCP协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP协议是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。
UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。
选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。
5.小结
计算机网络的基本概念主要有:
- 计算机网络:由两台或更多计算机组成的网络;
- 互联网:连接网络的网络;
- IP地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
- 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
- 网络协议:互联网使用TCP/IP协议,它泛指互联网协议簇;
- IP协议:一种分组交换传输协议;
- TCP协议:一种面向连接,可靠传输的协议;
- UDP协议:一种无连接,不可靠传输的协议。
二、TCP通信
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
两端通信时步骤:
- 服务端程序,需要事先启动,等待客户端的连接。
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在Java中,提供了两个类用于实现TCP通信程序:
- 客户端:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 - 服务端:
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。1.Socket类
构造方法
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
构造举例,代码如下:
Socket client = new Socket("127.0.0.1", 6666);
成员方法
public InputStream getInputStream()
: 返回此套接字的输入流。- 如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。
- 关闭生成的InputStream也将关闭相关的Socket。
public OutputStream getOutputStream()
: 返回此套接字的输出流。- 如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。
- 关闭生成的OutputStream也将关闭相关的Socket。
public void close()
:关闭此套接字。- 一旦一个socket被关闭,它不可再使用。
- 关闭此socket也将关闭相关的InputStream和OutputStream 。
public void shutdownOutput()
: 禁用此套接字的输出流。public ServerSocket(int port)
:使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
构造举例,代码如下:
ServerSocket server = new ServerSocket(6666);
成员方法
- 【服务端】启动,创建ServerSocket对象,等待连接。
- 【客户端】启动,创建Socket对象,请求连接。
- 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
- 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
- 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
到此,客户端向服务端发送数据成功。
自此,服务端向客户端回写数据。
- 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
- 【客户端】Scoket对象,获取InputStream,解析回写数据。
- 【客户端】释放资源,断开连接。
服务器端实现:
public class ServerTcp {
public static void main(String[] args) throws IOException {
//1.创建服务端对象
ServerSocket ss = new ServerSocket(7788);
//2.监听客户端连接,并获得套接字对象
System.out.println("等待客户端连接>>>");
Socket s = ss.accept();
//3.获得输入流,读数据,并把数据显示在控制台
InputStream is = s.getInputStream();
//int len = read(byte[] b) 从输入流读取字节数,并将它们存储到缓冲区 b,返回输入流字节长度
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys,0,len);
System.out.println("服务端接收到的数据是:"+data);
//3.获得输出流向客户端发送数据
OutputStream outputStream = s.getOutputStream();
outputStream.write(data.toUpperCase().getBytes());
//4.释放资源
s.close(); //可以不用写
ss.close();
}
}
客户端实现:
public class ClientTcp {
public static void main(String[] args) throws IOException {
//1.创建socket对象
Socket s = new Socket("127.0.0.1",7788);
//2.获得输出流,写数据
OutputStream outputStream = s.getOutputStream();
outputStream.write("我是tcp客户端,我非常喜欢java编程".getBytes());
//2.获得输入流,接收数据
InputStream inputStream = s.getInputStream();
byte[] bys = new byte[1024];
int len = inputStream.read(bys);
System.out.println("收到服务端的回复:"+new String(bys,0,len));
//3.释放资源
s.close();
}
}
4.TCP案例练习
练习1
- 客户端:发送数据,接收服务端反馈
- 服务端:接收数据,给出反馈
练习2
- 客户端:数据来自键盘录入,直到输入的数据是886,发送数据结束
- 服务端:接收到的数据在控制台输出
练习3
- 客户端:数据来自键盘录入,直到输入的数据是886,发送数据结束
- 服务端:接收到的数据写入文本文件
练习4(上传文件)
- 客户端:数据来自文本文件
- 服务端:接收到的数据写入文本文件
文件上传案例原理分析:
文件上传分析图解:
- 【客户端】输入流,从硬盘读取文件数据到程序中。
- 【客户端】输出流,写出文件数据到服务端。
- 【服务端】输入流,读取文件数据到服务端程序。
- 【服务端】输出流,写出文件数据到服务器硬盘中。
客户端实现:
public class UploadClient {
public static void main(String[] args) throws IOException {
//1.
Socket sock = new Socket("127.0.0.1",7788);
//2.准备数据源
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./1.jpg"));
//3.输出流
BufferedOutputStream bos = new BufferedOutputStream(sock.getOutputStream());
byte[] bys = new byte[1024];
int len=0;
while ((len = bis.read(bys))!= -1){
bos.write(bys,0,len);
bos.flush();
}
//注意:上传成功如果没有结束标记,可以使用:shutdownOutput() 禁用此套接字的输出流。
System.out.println("文件上传成功!");
//4.
sock.close();
}
}
服务器端实现:
public class UploadServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(7788);
Socket s = ss.accept();
//输入流-获取客户端发送的数据
BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
//输出流-保存文件到本地
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./copy1.jpg"));
//读写数据
byte[] b = new byte[1024];
int len = 0;
while ((len = bis.read(b))!=-1){
bos.write(b,0,len);
bos.flush();
}
System.out.println("服务端收到文件");
//关闭
s.close();
bis.close(); //记得要关闭文件
ss.close();
}
}
文件上传优化分析:
- 文件名称写死的问题
服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
if(!(f1.exists())){//准备文件夹
File f1 = new File("./upload");
f1.mkdirs();
}
//不重复的文件名
String filename = “xjt”+System.currentTimeMillis()+ new Random().nextInt(100)+”.mkv”;
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+”//“+filename)); ``` 循环接收的问题 服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
// 每次接收新的连接,创建一个Socket
while(true){
Socket accept = serverSocket.accept();
......
}
多线程提高效率
服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:while(true){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
优化实现-服务端
public class UploadServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(7788);
while(true){
Socket s = ss.accept();
//线程池
ExecutorService ec = Executors.newFixedThreadPool(10);
ec.submit(() ->{
try{
//输入流-获取客户端发送的数据
BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
//输出流-保存文件到本地
File f1 = new File("./upload");
if(!(f1.exists())){
f1.mkdirs();
}
//文件名称不重复
String filename = "xjt"+System.currentTimeMillis()+ new Random().nextInt(100)+".mkv";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+"//"+filename));
//读写数据
byte[] b = new byte[1024];
int len = 0;
while ((len = bis.read(b))!=-1){
bos.write(b,0,len);
bos.flush();
}
System.out.println("服务端收到文件,当前线程是:"+Thread.currentThread().getName());
//关闭
s.close();
bis.close(); //记得要关闭文件
}catch (Exception e){
e.printStackTrace();
}
});
}
//关闭ServerSocket对象
//ss.close(); //这里死循环就不用关了
}
}
练习5
- 客户端:数据来自文本文件,接收服务端反馈
- 服务端:接收到的数据写入文本文件,给出反馈
出现问题:程序一直等待 原因:读数据的方法是阻塞式的 解决办法:自定义结束标记,使用shutdownOutput() 方法
练习6
- 客户端:数据来自文本文件,接收服务端反馈
- 服务端:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程
练习6
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。
案例分析:
- 准备页面数据,web文件夹。
复制到我们Module中,比如复制到day08中 - 我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问
- 服务器程序中字节输入流可以读取到浏览器发来的请求信息
GET/web/index.html HTTP/1.1是浏览器的请求消息。/web/index.html为浏览器想要请求的服务器端的资源,使用字符串切割方式获取到请求的资源。
//转换流,读取浏览器请求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出请求资源的路径
String[] strArr = requst.split(" ");
//去掉web前面的/
String path = strArr[1].substring(1);
System.out.println(path);
案例实现
服务端实现:
public class SerDemo {
public static void main(String[] args) throws IOException {
System.out.println("服务端 启动 , 等待连接 .... ");
// 创建ServerSocket 对象
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
// 转换流读取浏览器的请求消息
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
// 取出请求资源的路径
String[] strArr = requst.split(" ");
// 去掉web前面的/
String path = strArr[1].substring(1);
// 读取客户端请求的资源文件
FileInputStream fis = new FileInputStream(path);
byte[] bytes= new byte[1024];
int len = 0 ;
// 字节输出流,将文件写会客户端
OutputStream out = socket.getOutputStream();
// 写入HTTP协议响应头,固定写法
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
// 必须要写入空行,否则浏览器不解析
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
server.close();
}
}
访问效果
- 火狐
小贴士:不同的浏览器,内核不一样,解析效果有可能不一样。
发现浏览器中出现很多的叉子,说明浏览器没有读取到图片信息导致。
浏览器工作原理是遇到图片会开启一个线程进行单独的访问,因此在服务器端加入线程技术。
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while(true){
Socket socket = server.accept();
new Thread(new Web(socket)).start();
}
}
static class Web implements Runnable{
private Socket socket;
public Web(Socket socket){
this.socket=socket;
}
public void run() {
try{
//转换流,读取浏览器请求第一行
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出请求资源的路径
String[] strArr = requst.split(" ");
System.out.println(Arrays.toString(strArr));
String path = strArr[1].substring(1);
System.out.println(path);
FileInputStream fis = new FileInputStream(path);
System.out.println(fis);
byte[] bytes= new byte[1024];
int len = 0 ;
//向浏览器 回写数据
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
}catch(Exception ex){
}
}
}
}
访问效果:
图解:
三、UDP编程
1.UDP发送/接收数据基本步骤
发送:
接收:DatagramPacket
构造方法:
DatagramPacket(byte[] buf, int length)
构造一个 DatagramPacket用于接收长度的数据包 length 。
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length)
构造一个 DatagramPacket用于接收长度的分组 length ,指定偏移到缓冲器中。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int length, SocketAddress address)
构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。
DatagramPacket
实例对象的方法:
DatagramPacket dp = new DatagramPacket(receive_data,receive_data.length);
dp.getData(); //获得缓冲区数据,字节数据,需要new String() 转为字符串
dp.getLength() //返回要发送的数据的长度或接收到的数据的长度
Modifier and Type | Method and Description |
---|---|
InetAddress |
getAddress() 返回该数据报发送或接收数据报的计算机的IP地址。 |
byte[] |
getData() 返回数据缓冲区。 |
int |
getLength() 返回要发送的数据的长度或接收到的数据的长度。 |
int |
getOffset() 返回要发送的数据的偏移量或接收到的数据的偏移量。 |
int |
getPort() 返回发送数据报的远程主机上的端口号,或从中接收数据报的端口号。 |
SocketAddress |
getSocketAddress() 获取该数据包发送到或正在从其发送的远程主机的SocketAddress(通常为IP地址+端口号)。 |
void |
setAddress(InetAddress iaddr) 设置该数据报发送到的机器的IP地址。 |
void |
setData(byte[] buf) 设置此数据包的数据缓冲区。 |
void |
setData(byte[] buf, int offset, int length) 设置此数据包的数据缓冲区。 |
void |
setLength(int length) 设置此数据包的长度。 |
void |
setPort(int iport) 设置发送此数据报的远程主机上的端口号。 |
void |
setSocketAddress(SocketAddress address) 设置该数据报发送到的远程主机的SocketAddress(通常是IP地址+端口号)。 |