社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
今天我们涉及到了Java比较重要的部分,网络编程!说到网络编程我们应该天天在用,Java语言就是一种很合适的后台语言,那既然它可以做后台,就必须涉及到前后端交互,所以网络编程就是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来,以此实现信息的交互;
在学习网络编程之前我们应该先对网络有一个大致的了解;而这里我们就需要先理解一个概念,即网络世界的七层结构模型;
计算机网络的七层模型:
物理层处于OSI的最底层,是整个开放系统的基础。物理层涉及通信信道上传输的原始比特流(bits),它的功能主要是为数据端设备提供传送数据的通路以及传输数据。
数据链路层的主要任务是实现计算机网络中相邻节点之间的可靠传输,把原始的、有差错的物理传输线路加上数据链路协议以后,构成逻辑上可靠的数据链路。需要完成的功能有链路管理、成帧、差错控制以及流量控制等。其中成帧是对物理层的原始比特流进行界定,数据链路层也能够对帧的丢失进行处理。
网络层涉及源主机节点到目的主机节点之间可靠的网络传输,它需要完成的功能主要包括路由选择、网络寻址、流量控制、拥塞控制、网络互连等。
传输层起着承上启下的作用,涉及源端节点到目的端节点之间可靠的信息传输。传输层需要解决跨越网络连接的建立和释放,对底层不可靠的网络,建立连接时需要三次握手,释放连接时需要四次挥手。
会话层的主要功能是负责应用程序之间建立、维持和中断会话,同时也提供对设备和结点之间的会话控制,协调系统和服务之间的交流,并通过提供单工、半双工和全双工3种不同的通信方式,使系统和服务之间有序地进行通信。
表示层关心所传输数据信息的格式定义,其主要功能是把应用层提供的信息变换为能够共同理解的形式,提供字符代码、数据格式、控制信息格式、加密等的统一表示。
应用层为OSI的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。
上面的七层网咯模型就组成了我们当今的计算机网络;
但是上面的模型虽然很详细的对计算机网络进行了说明,但是还是比较抽象,在应用的层面上来说,其实我们可以将七层的网络模型简化为四层,即
应用层
这一层的功能就好比我们如何在这个网络中找到特定的网络主机;
它在我们生活中常用的协议有
http 超文本传输协议(访问网页)
telnet 远程登录
ssh 远程登录(保证安全)
传输层
这一层的功能就好比我们找到了主机,但是主机上有很多的服务,我们所要应用的服务是哪一个呢?它可以帮我们打包数据并确定目的应用程序;而想要确定我们具体应用的是什么服务就需要一个叫做端口的东西,来定位我们的目标程序;
一些常见应用所占用的端口:
http 占用80 端口
telnet 22
ssh 22
mysql 3306
还有我们需要知道数据传输的两种协议即
TCP协议(保障数据的可靠有序), UDP协议 不保证
TCP:TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
UDP:UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。
互联网层
IP协议
网络访问层
有了上面的基础我们就正式的开始学习Java网络编程了,上面我们说到Java为我们提供了一些很方便的接口,方便我们进行网络编程,这里我们就先学习两种;
他也被我们叫做套接字,socket编程使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。
当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。
java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。
废话不多说我们上代码;
比如我们现在要写一个客户端程序,它实现的就是简单的http请求,请求一个时间的
import java.io.*;
import java.net.Socket;
// Socket 端点 底层是TCP协议
public class TestSocket {
public static void main(String[] args) throws IOException {
// 1. 新建Socket对象
// host: 主机ip地址 port:端口号
Socket socket = new Socket("192.168.X.XX", 80);
// 2. 发送数据用输出流
OutputStream out = socket.getOutputStream();
out.write("GET /time HTTP/1.1n".getBytes());
out.write("Host: localhostn".getBytes());
out.write("n".getBytes());
out.write("n".getBytes());
// 3. 接收响应用输入流
InputStream in = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf-8"));
while(true) {
String line = reader.readLine();
if(line == null) {
break;
}
System.out.println(line);
}
socket.close();
}
}
上面的代码我们可以看到我们先新建了一个socket对象,它参数中的IP地址和端口号帮我们连接了某台服务器主机;
然后我们作为客户机要向服务器发送请求,socket.getoutputstream()方法是获取我们给主机发送的请求内容,这里我们可以直接编辑好,也可以定义一个输入流,通过键盘现场编辑我们要发送的内容;
Scanner sc = new Scanner(System.in);
while(sc.hasNextLine()){
String s1 = sc.nextLine();
out.write(s1.getBytes("utf-8"));
}
上面由于我们定义的是字节的输出流,所以我们的字符串请求内容要getbytes(),得到他的字节输出流;传给服务器;
然后我们又定义了一个输入流,接受服务器的响应;
InputStream in = socket.getInputStream();
然后用一个高效的缓存流帮我们读取数据;
再下面的代码就是循环读取服务器的响应内容;
上面的代码就是用socket套接字方法所写的最简单的客户端代码;我们发现这个简答的代码并不支持多线程,也没有考虑线程的安全问题;
所以我们最好是用线程池的方法,线程池能很高效的为每个连入的客户端分配线程;
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 5000);
OutputStream out = socket.getOutputStream();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor
(10, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
poolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
out.write(line.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
InputStream in = socket.getInputStream();
while(true) {
byte[] buf = new byte[1024];
int len = in.read(buf);
if(len == -1) {
break;
}
String result = new String(buf, 0, len, "utf-8");
System.out.println(result);
}
}
上面的代码就是用了线程池的版本,同样我们先创建了socket对象,然后socket.getoutoutstream(),获取一个输出流,负责向服务器提交请求;
然后我们手动定义了一个线程池,这里的参数是需要注意的,
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
@NotNull TimeUnit unit,
@NotNull BlockingQueue<Runnable> workQueue)
我们一个个分析,手动创建线程池中int corePoolSize 代表核心线程数,也就是线程池中的最小线程数,然后我们再看BlockingQueue< Runnable > workQueue,这代表阻塞队列,当核心线程都被占用,这时又新进来了别的线程,就会先排队到阻塞队列等待空闲线程,这个阻塞队列可以是数组队列,new ArrayBlockingQueue(),也可以是链表的队列,即new LinkedBlockingQueue(),这个阻塞队列的长度也是可以赋值的;上面我们用的是链表队列,并没有给它赋大小;可如果并发量还多,阻塞队列也排满了怎么办呢?这时就用到了int maximumPoolSize,这个参数 是代表线程池的最大线程数,阻塞队列满了后我们线程池就会继续开几个救急线程,但是前提是不能超过最大的线程数;当最大线程数都满足不了需求,这时就会抛出异常声明; long keepAliveTime,这个参数是代表线程的存活时间,就是说当我们并发量降下来的时候,救急线程空闲下来,这时当这个线程的空闲时间超过我们定义的时间,JVM虚拟机就会自动回收该线程;TimeUnit unit,这个参数就是我们上面定义的时间的单位,比如上面的代码我们给的时间单位就是秒(second);
这个方式创建的线程池就是Executors.newFixedThreadPool(int);即线程池大小确定的线程池,我们给的核心线程数和最大线程数一样,同时给了0的线程的存活时间,即没有线程的消亡;
上面我们写了socket编程的简单客户端,可是要实现信息交互,光有客户端提交请求是不够的,我们还需要有服务器对他的请求进行回应;下面我们就看看服务器端的编写;
服务器端是服务端,它只需要声明我们开启服务的端口即可;
因此我们应该调用ServerSocket()接口,创建一个服务器对象;
ServerSocket serverSocket = new ServerSocket(5000)
之后用accept监视端口看是否有客户段访问,有就获取他的请求;
public class Server1 {
// 第一个服务器,阻塞io
public static void main(String[] args) throws IOException {
// 1. 创建serverSocket
ServerSocket serverSocket = new ServerSocket(5000);
System.out.println("服务已启动,等待连接");
ExecutorService service = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
while(true) {
// 2. 调用accept 等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端已连接....");
// 使用了线程池来处理io
service.submit(() -> {
// 把io相关的操作放在线程内执行,让每个线程处理一个io操作,避免io阻塞
try {
handle(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
private static void handle(Socket socket) throws IOException {
// 3. 接收客户的输入
InputStream in = socket.getInputStream();
// 4. 拿到输出流向客户写入
OutputStream out = socket.getOutputStream();
while (true) {
byte[] buf = new byte[1024];
int len = in.read(buf);
if (len == -1) {
break;
}
String echo = new String(buf, 0, len, "utf-8");
System.out.println(echo);
out.write(("服务器回答:" + echo).getBytes("utf-8"));
}
}
}
这里我们也还是用线程池的方法处理;在这里我们只是简单的拿到客户端数据,并返回同样的信息;只是一个简单响应,如果要实现更多的响应,就可以对他的响应做进一步处理,然后out.write()将响应写回去;
这里我们需要考虑多线程多客户段访问的问题;所以用while循环,等待多客户段的连接,并分给每个客户端一个线程,专门处理他的请求;
以上就是服务器和客户段的交互过程;
除了Socket编程,我们Java还提供了url的包装方式,帮我们进行特定的http请求的提交和处理;
url即为统一资源定位符;
public class TestURL {
public static void main(String[] args) throws IOException {
// URL 统一资源定位符
// http://ip地址:端口/资源地址
// 127.0.0.1 <==> localhost
HttpURLConnection connection = (HttpURLConnection)
new URL("http://192.168.X.XXX:80/img/chrome.png").openConnection();
// GET /index.html HTTP/1.1
// Host: localhost
InputStream in = connection.getInputStream();
FileOutputStream image = new FileOutputStream("e:\2.png");
while(true) {
byte[] buf = new byte[1024*8];
int len = in.read(buf);
if(len == -1) {
break;
}
image.write(buf, 0, len);
}
image.close();
connection.disconnect();
}
}
上面的例子我们用
HttpURLConnection connection = (HttpURLConnection)
new URL(“http://192.168.X.XXX:80/img/chrome.png”).openConnection();
新建了一个httpurlconnection的http请求,这里我们看到,url方式和socket方式的不同是url的参数是一个精确的网址,包括端口号和具体请求地址;
openconnection(),和connection.disconnect()是开启和关闭连接的方法;上面的例子我们完成的是特定网络页面的图片的下载;我们获取了图片的数据;然后复制下来,就等同于下载了图片;
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!