网络编程的概念
问题:为什么会出现网络编程这个东西呢?
答: 地球村-->随着互联网的发展,交通方式的进步,缩小了人与人之间的时空距离,整个世界紧缩成了一个"村落"
计算机网络-->是指将地理位置不同的多台计算机及其外部设备,通过线路连接起来,在网络操作系统、网络管理软件、网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统(计算机网络通常用一些通用的、可编程的硬件互连而成)
数据通信-->是计算机网络最主要的功能之一,数据通信是指按照一定的通信协议,利用数据传输技术在两个终端之间传递数据信息的一种通信方式和通信业务,可以实现计算机与计算机、计算机与终端以及终端与终端之间的数据信息传递
网络编程可以简单的理解为是对信息的发送与接收,中间传输为物理线路的作用
网络编程中最核心的是在发送端把信息通过规定好的协议 进行"打包",在接收端按照规定好的协议进行"解析",从而提取出对应的信息以达到通信的目的
网络模型
OSI七层模型
OSI(Open System Interconnect),即开放式系统互联。一般都叫做OSI参考模型,是ISO(国际标准化组织)在1985年研究的网络互连模型
接下来我将逐一介绍每层的内容及作用,并且通过一个具体的例子来理解(甲方向乙方通过网络通信发送一个"Hello")
1.应用层
OSI模型中最接近用户的一层,是为计算机用户提供应用接口,也提供各种网络服务,常见的网络服务协议有HTTP、HTTPS、FTP、POP3、SMTP
应用层: 最直观的理解就是在人机交互页面、系统程序窗口,甲方输入了一个"Hello"
2.表示层
表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据可以被另一个系统的应用层识别
计算机如何处理"Hello"这个数据,就是使用翻译,计算机只能识别二进制0、1,而表示层就是计算机将用户发送的数据翻译成自己的"文字"(表示层还有安全加密、压缩等功能)
3.会话层
会话层就是负责建立、管理和终止表示层实体之间的通信会话,该层的通信由不同设备中的应用程序之间的服务请求和响应组成。
计算机知道你要发送的东西之后,就需要准备发送了。那么,第一步就是要找到对方(乙方),并和对方建立会话关系。直接理解:会话属于软件层面,允许不同机器上的用户之间建立会话关系。
4.传输层
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,我们常说的TCP/UDP就是在这一层,端口号就是这里的端
传输层可理解为是同一个软件中的两个端口进行数据传输。我用微信发送的消息,你也需要用微信来接收。那么就是电脑端微信用户之间的传输(只在乎起点和终点)
5.网络层
网络层通过IP寻址来建立两个节点之间的连接,通常说的IP层就是这一层,IP是Internet的基础
传输层通过微信进行传输已经知道了,但是微信用户这么多,怎么准确无误的是让甲方发送给乙方呢?就是使用IP地址,选择最佳路径进行数据传输
6.数据链路层
将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测,在物理层提供的服务基础上,在通信的实体间建立数据链路连接,传输的数据单位是"帧",并采用差错控制与流量控制方法,使有差错的物理线路变成无差错的数据链路。
知道甲方和乙方的IP后,怎样进行他们之间的传输就是数据链路层需要考虑的事
7.物理层
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。
到达物理层后变成电信号,到达目标主机开始一个逆向的过程通过数据链路层--到应用层此时乙方的电脑上就可以看到甲方发送的"Hello"
TCP/IP五层模型
这五层模型实际上就是简化了的OSI七层模型,将应用层、表示层、会话层都归为应用层
接下来再举一个例子来理解这五层的作用
比如现在你在网上买了一件衣服
物理层就是下单需要写收件人地址、收件人姓名,商家就会根据这个地址给我发货
数据链路层就是负责相邻的节点之间,怎样进行传输(运输的过程中相邻的地方采用什么方式进行运输,汽车、飞机?)
网络层主要负责走哪条路划算(快递公司层面运输快递可以有好几条线路)
传输层就是站在我和商家这个层面只在乎起点和终点是哪(端到端之间的传输)
最后应用层描述了传输的数据,用户怎么样来使用
越往上越接近用户,越往下越接近硬件
在网络分层这里,相当于上层协议要调用下层协议,下层协议给上层协议提供服务
应用层在这里考虑的比较少,所以很多时候也叫做TCP/IP四层协议
网络传输的基本流程
我们以QQ发送消息为例,A给B发送一个hello
1.应用层
用户在输入框中输入hello这个字符串,qq这个应用程序就会把这个字符串,给构建成一个应用层数据报
每个应用程序的数据报都不一定是一样的,是开发这个应用程序的程序员定义的
这里我们假定一个qq的应用层数据报
所谓的数据报,本质上就是一个遵守一定约定格式的字符串
接下来程序要调用操作系统的api,把这个应用层数据交给传输层
2.传输层
在传输层中,就要把上述应用层数据构造成传输层的数据报,传输层使用到的协议,最知名的就是UDP和TCP,比如此时这里使用的是UDP,就需要构造出UDP数据报(在应用层数据报的基础上,拼接一个UDP报头)
UDP报头是另外一个特定格式的字符串(涉及到源端口和目的端口后面详细讲解),报头就是一个"标签",通过标签表示当前要把这个消息怎样进行传输
接着传输层就把这个UDP数据报交给网络层
3.网络层
网络层中最知名的协议就是IP协议,IP协议要基于上述数据,打包成一个IP数据报
IP报头也是一个字符串,包含了另外一组消息(其中最核心的消息是源IP和目的IP后面详细介绍)
网络层数据报准备好,还要进一步交给数据链路层
4.数据链路层
数据链路层最知名的协议叫做"以太网",基于上述数据,还要打包成一个"以太网数据帧"
继续往下传输交给物理层
5.物理层
把上述二进制的数据(0101...)转换成 电信号/光信号,此时就真正的把数据给发送出去了
上方的五个过程是发送方的工作,那么接收方如何工作呢?
1.在物理层网卡接收的是光信号和电信号,在物理层将光电信号转换回二进制的数据,转换回的数据其实就是一个"以太网数据帧"
2.接着把这个数据交给数据链路层解析,数据链路层需要去掉帧头帧尾,取出中间的载荷,交给网络层,以太网帧头会记录这个载荷是不是IP数据报
3.网络层,IP协议对这里进行解析,解析出IP报头,取出IP协议的载荷,把这里得到的传输层数据报,交给传输层,IP报头里会记录这个载荷是UDP还是TCP
4.传输层,UPD再进行解析,取出报头,取出载荷,再把这个载荷交给对应的应用程序(UDP报头里有一个重要的字段"目的端口",目的端口是和一个具体的应用程序关联在一起的)
5.到了应用层,qq应用程序,qq就会根据应用层协议进行解析,将内容显示到界面上
网络编程
网络编程的核心,Socket API 是操作系统给应用程序提供的网络编程的API,可以认为socket api是和传输层密切相关的,传输层里,提供了两个最核心的协议,UDP和TCP,因此socket api也有了两种风格UDP、TCP
1.简单认识一下TCP和UDP
有无连接可以理解为打电话和发微信,打电话需要对方有"接受"这个动作(连接建立),才能通信,所以是有连接,而发微信只要发就行了
可不可靠传输理解为发送方是否知道自己的消息发送过去了,还是丢了,比如打电话和发微信,打电话你可以明确的问你听到了吗,那么根据对方的反应你就能知道自己的消息发送过去了没,而发微信不能知道自己的消息是否发送过去了,所以是不可靠传输(如果实现已读功能,那么也是可靠传输)
面向字节流:数据传输就和"文件读写"类似,"流式"的面向数据报:数据传输以一个个的"数据报"为基本单位(一个数据报可能是若干个字节,带有一定格式)
全双工:指的是一个通信通道可以双向传输(既可以发送也可以接收)
2.基于UDP的客户端服务器的网络通信程序
(1).了解API
DatagramSocket API
使用这个类,表示一个socket对象(在操作系统中,把这个socket对象也是当成一个文件来处理的,相当于是文件描述符上的一项),一个socket对象就可以和另外一个主机进行通信了
DatagramSocket()方法:这个是无参的构造方法,表示创建一个socket对象(没有指定端口,系统会自动分配一个空闲的端口)
DatagramSocket(int port)方法:这个带参数的构造方法是需要传入一个端口号,此时就是让当前的socket对象和这个指定的端口关联起来
receive(DatagramPacket p)方法:此处传递的相当于是一个空的对象,receive内部会对这个空对象进行填充,从而构造出结构(参数也是一个"输出型参数")
send(DatagramPacket p)方法:表示发送一个packet
close()方法:释放资源(用完之后记得关闭)
DatagramPacket API
这个类表示UDP中传输的一个报文
DatagramPacket(byte[] buf,int length)方法:构造一个DatagramPacket用来接收 数据报,接收的数据保存在字节数组里,接收指定长度length
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address)方法:构造一个DatagramPacket用来发送数据报,发送的数据为字节数组,从0到指定长度(length),address指定目标主机的IP和端口号
InetAddress getAddress()方法:从接收的数据报中,获取发送端主机IP地址,或从发送的数据报中,获取接收端主机的IP地址
Int getPort()方法:从接收的数据报中,获取发送端的主机端口号,或从发送的数据报中,获取接收端的主机端口号
byte[] getData()方法:获取数据报中的数据
(2).编写UPD客户端服务器程序(回显)
这里我们编写的服务器是"回显服务器",什么是回显服务器,一个普通的服务器收到请求,根据请求计算响应,发送响应是这样一个流程,而根据请求计算响应就是业务逻辑,但是我们这里没有实际的业务所以就省去了根据请求计算响应这个步骤,就是接收什么我们就给客户端发送什么
我们先编写服务器代码
java
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP版本的回显服务器
public class UdpEchoServer {
//网络编程,本周上是操作网卡
//但是网卡不方便直接操作,在操作系统内核中,使用"socket"这样的文件来抽象表示网卡
//因此进行网络通信,需要先有一个socket对象
private DatagramSocket socket = null;
//对应服务器来说,创建socket对象的同时,要绑定一个具体的端口号
//服务器一定要关联一个具体的端口
//服务器是网络传输中被动的一方,如果操作系统随机分配端口,那么客户端就不知道这个端口是谁了
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器给多给客户端提供服务
while (true){
//只要有客户来就提供服务
//1.读取客户端发送的请求
//receive方法参数是一个输出型参数,需要构建一个空白的DatagramPacket对象交给receive进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//此时这里的DatagramPacket是一个特殊的对象,不方便直接处理,需要转换成字符串进行处理
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端,send的参数也是DatagramPacket,需要把这个Packet对象构造好
// 此处的响应对象,不能是空的字节数组,需要有对应的内容(使用响应进行构造)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());//获取到客户端的IP和端口号
socket.send(responsePacket);
//4.打印一些,当前这次请求响应的处理中间结果
System.out.printf("[%s:%d] req: %s;resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
接着是客户端代码
java
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//UDP版本的客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取要发送的数据
System.out.print("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodbye");
break;
}
//2.构造成UDP请求,并发送
// 构造这个Packet的时候,需要将serverIp和serverPort都传入进去,但是此处的IP地址需要填写的是一个32位的整数形式
// 上述的IP是一个字符串,需要用InetAddress.getByName进行转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.读取服务器的UDP响应并且解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.把解析好的结果打印处来
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
这里我们需要注意几个点
1.这里的receive方法的参数是一个输出型参数,receive内部会根据参数对象填充数据,填充的数据来自网卡,所以我们之后在处理这个接收的DatagramPacket的时候需要对其进行处理(转成字符串)
2.这里对对象进行处理转字符串的操作,上述给的最大字节长度是4096,但是实际上可能不用那么多,requestPacket是以字节数组作为存储数据的缓冲区,我们构造字符串用了哪些部分就构造那些部分就好,通过使用getlength获取到实际的数据报长度
3.这个是数据通过网卡到字节缓冲区的过程
根据这个UDP回显服务器,我们可以写一个类似字典的服务器,给我们一个单词返回他的意思
很明显,这和之前的代码基本上就是一样的,就是在根据请求计算响应的时候需要做出改动
java
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer{
Map<String,String> map = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//插入字典内容
map.put("cat","小猫");
map.put("abondon","放弃");
map.put("dog","小狗");
map.put("fuck","XX");
map.put("thanks","谢谢");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"没有该单词!");
}
public static void main(String[] args) throws IOException {
UdpDictServer dictServer = new UdpDictServer(9090);
dictServer.start();
}
}
注意:一个端口只能被一个进程使用,而一个进程可以使用多个端口
3.基于UDP的客户端服务器的网络通信程序
(1).了解API
ServerSocket
专门给服务器使用的Socket对象
ServerSocket(int port)方法:创建一个服务端Socket,并绑定指定端口
Socket accept()方法:开始监听指定端口,有客户端连接后,返回一个Socket对象,并且基于Socket建立与客户端的连接,否则阻塞等待(因为TCP是有连接的(类似于接电话操作))
Socket
是既会给客户端使用,也会给服务器使用
在服务器这边,由accpet返回
在客户端这边,代码里构造,构造的时候指定一个IP和端口号(服务器的IP和端口号),这样就可以和服务器建立连接了
InputStream getInputStream()方法
OutputStream getOutputStream()方法:进一步通过Socket对象,获取到内部的流对象,借助流对象来进行发送/接收
(2).编写TCP客户端服务器程序(回显)
先看服务器代码
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//Socket构造方法,能够识别点分十进制(127.0.0.1)的IP格式,比DatagramPacket更方便
//new这个对象的同时,就会进行Tcp的连接操作
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while (true){
//1.从键盘上读取用户输入的内容
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodby");
break;
}
//2.把读取的内容构造成请求,发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//此处加上flush确保发出去了
printWriter.flush();
//3.读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.把响应内容显示到页面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
在看看客户端代码
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
//使用线程池解决多线程频繁创建销毁问题
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true){
//使用clientSocket和具体的客户端进行交互
Socket clientSocket = serverSocket.accept();
//使用多线程处理
//但是这里的多线程版本会涉及到频繁的创建和销毁线程操作
// Thread thread = new Thread(()->{
// processConnection(clientSocket);
// });
// thread.start();
//使用线程池
threadPool.submit(()->{
processConnection(clientSocket);
});
}
}
//使用这个方法来处理一个连接
//这一个连接对应一个客户端,但是这里可能会涉及到多次交互
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述socket对象和客户端进行通信
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//由于要处理多个请求和响应,也是用循环来进行
while (true){
//1.读取请求
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//没有下一个数据,说明读完了(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//注意!!此处使用next是一直要读取到换行符/空格/其他空白符结束,但是最终返回结果不包含上述空白符
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.返回响应结果
//OutputStream没有write String这样的功能,可以把String里的字节数组拿出来进行写入
//也可以使用字符流进行转换
PrintWriter printWriter = new PrintWriter(outputStream);
//此处使用println来写入,让结果带有一个\n换行,方便对端接收解析
printWriter.println(response);
//flush用来刷新缓冲区,保证当前的数据确实发出去了
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s \n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}