Java-Web学习-Java基础-网络编程(TCP和UDP)附TCP实现通信
网络编程
“Java是Internet上的语言”,其从语言级别上提供了完备的对网络应用程序的支持,联网的具体底层细节被封装在Java提供的网络类库中,而向所有平台提供统一的网络编程环境。
计算机网络基础
网络编程的目的就是通过网络协议实现和其他计算机的数据交换和通讯,而这个目的带来两个主要问题:
- 如何定位复杂网络系统中的特定主机?
- 如何在定位后进行可靠高效的数据传输?
其中第一个问题是通过IP地址+端口解决,IP地址定位主机位置,端口号定位主机上的特定应用,两者组合得到套接字(Socket)。而第二个问题这通过网络通信协议解决,而我们事实上的国际网络通信协议标准是TCP/IP参考模型
IP与Port
IP -> InetAddress类
其唯一标识Internet上的一个通信实体,而任何一个实体能够通过唯一的回环地址(127.0.0.1)指向自身。我们通常能够接触到的比如www.baidu.com这种称为“域名”,其是Internet上主机的另一种标识方式,本机回环使用的域名是localhost。当使用域名进行连接时,需要由域名服务器(DNS)进行解析,将域名转换成目标IP地址才能建立连接,这是域名解析的过程。
IP地址共有两种分类方式:
- IPV4:由4个字节组成,已经被用尽,以点分十进制标识,比如123.165.0.1
- IPV6:由16个字节组成,分为8个无符号整数,每个整数用4位十六进制数表示,以冒号分开,比如ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
InetAddress类也因此分为两个子类:Inet4Address和Inet6Address。该类没有公有构造方法,需要通过getByName()或getLocalHost()方法返回对应的实例。可以通过getHostName()或getHostAddress()获取相关信息。可以发现,Java把域名解析、连接DNS服务器等底层细节都封装地非常完全。
Port
端口是主机用于标识目前正在计算机上运行的应用,或者我们可以称为程序。为了规范,我们对端口有一套公认的分配标准:
- 0~1023:公认端口,已经被预定义的通信服务程序占据,它们与一些服务紧密绑定,通常是一些通信协议。
- 1024~49151:注册端口,可以被分配给用户进程或应用。
- 49152~65535:动态/私有端口,理论上,不应为服务分配这些端口。
网络通信协议
网络通信协议,我们耳熟能详,但是它究竟规定了些什么呢?网络通信协议覆盖的范围极广,比如压缩解压缩、如何控制流量、如何指定地址、如何进行加密,这也使得网络通信协议非常复杂。
TCP/IP采用了通信协议分层的思想,将一个复杂的通信协议分解为多个协议,并分层管理:上一层调用下一层而不能与再下面的一层发生关系,同层间可通信。
TCP/IP中的TCP(Transmission Control Protocol)和IP(Internet Protocol)只是TCP/IP协议簇中的两个,事实上,TCP/IP是一个极为庞大的多个协议组成的协议簇,从顶到底分为应用层、传输层、网络层、物理+数据链路层四层,其中TCP属于传输层,IP属于网络层,此外还有HTTP(属于应用层)、DNS(属于应用层)、Link(属于物理链路层)。
URL 定位
URL,Uniform Resource Locator,统一资源定位符。我们之前说,我们可以用IP地址+Port定位到Internet上任意一个主机上的应用,但是这其实还不够。我们不仅访问应用,还需要访问特定的资源,比如图片、视频等。同时,我们在链接到一个通信实体时并不知道他支持什么样的通信协议,贸然链接可能引起很多问题。
URL由五部分组成:
http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
- 传输协议:http
- 主机名:IP地址或域名
- 端口号:端口号
- 文件名:目标通信实体上的文件名称
- 片段名:某个资源片段名称,比如小说的某个章节号
- 参数列表:需要传递的参数名称,比如用户名等
Java下利用URL类来描述一个URL,并提供解析和构造方法。我们可以通过字符串来创建一个URL对象,并调用Java提供的方法来分析这个URL请求的究竟是什么资源。
Restful的风格
当我们使用URL访问同一个资源时,理论上不同的操作对应不同的参数列表(至多再对应不同的片段名),即我们使用参数列表进行传参,从而告诉服务端我们需要进行什么操作。
而Restful的风格鼓励我们使用请求地址传参,即将参数放在路径名中成为路径的一部分,而省去参数列表:
https://mygraph.cn/addTwoInt?a=100&b=1000 传统
https://mygraph.cn/addTwoInt/100/1000 restful风格
好处在于路径更加整洁;同时由于框架会自动进行类型转换,获取参数更加方便;以及参数异常时不再是方法内参数转化失败, 而是指示路径和方法不匹配。
TCP网络编程
TCP网络编程基础是Socket,即套接字。
Socket 套接字
Socket将网络连接视作一个流,数据在两个Socket间通过IO传输,分为两类:
- 流套接字:TCP下提供字节流服务
- 数据报套接字:UDP下的数据报服务
Java下的基于Socket的TCP编程
Java下TCP网络编程分为服务端编程和客户端编程。
客户端需要:
- 创建根据目标服务端建立通信套接字Socket对象,若对方未响应者报异常。
- 获取链接到目标Socket的输入输出流,这是基于Java的流传输相关的标准库的。
- 按照一定协议对Socket进行读/写:通过输入流读取服务器传来的信息,通过输出流将信息写进传输流。
- 关闭通信套接字Socket
服务端需要:
- 调用ServerSocket创建一个属于服务端的套接字对象,用于监听客户端
- 调用accept()方法,一旦接受连接,该方法返回一个通信套接字对象
- 获取该通信套接字的输入输出流
- 关闭ServerSocket和通信套接字(注意到这两个而是不一样的)
UDP网络编程
UDP是另一种传输层的网络通信协议,不同于TCP的流传输,其采用类似于消息的数据报方式传递信息。其将发送方和接收方的IP+port放到了数据报中
两个通信实体间通过互相发送数据报的方式传递信息,就像发送快递包裹一样。这带来很高的不可靠性,任何一方都不会关心对方是否正确接收数据。
但是这也使得双方能够在不建立连接的情况下进行通信,极大地减少了通信延迟,这使得当实时性需求很高时,UDP通信比TCP更加常见。同时这种“不负责任”的乱丢数据,也支持一对多的广播机制。
Java下的UDP编程
Java使用DatagramSocket类表示套接字,DatagramPacket类表示数据报。
发送方:
- 建立一个套接字对象socket代表发送方实体
- 建立一个数据报对象packet,并将接收方的IP地址+Port放到里面
- 调用套接字对象socket.send(packet)发送数据报
接收方:
- 建立一个套接字对象socket代表当前接收方
- 建立一个缓冲区byte[]并将它传入一个数据报对象从而进行关联
- 调用socket.receive(packet),进入等待接收状态
- 收到数据后,数据存储在与数据报对象关联的缓冲区里,通过调用packet.get*()来获得数据报对象内的所有信息。
实践代码(TCP实现)
// Server.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
ServerSocket server;
public Server(){
try{
this.server = new ServerSocket(8888);
} catch (Exception e){
e.printStackTrace();
}
}
public void serve(){
System.out.println("Qiume is online!");
while (true){
try{
Socket communication = this.server.accept();
OutputStream out = communication.getOutputStream();
synchronized (out){
out.write("Hello, who are U?".getBytes());
}
InputStream in = communication.getInputStream();
StringBuilder receivedMsg = new StringBuilder();
synchronized (in){
int c;
for (c = in.read(); in.available()!=0; c = in.read()) {
receivedMsg.append((char) c);
}
receivedMsg.append((char) c);
}
System.out.println("Qiume received:" + receivedMsg.toString());
synchronized (out){
out.write("I love you, too".getBytes());
}
communication.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// Client.java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
Socket communication;
public Client() {
}
public void start() {
System.out.println("Norton visits!");
try {
try {
this.communication = new Socket("localhost", 8888);
} catch (Exception e) {
e.printStackTrace();
}
InputStream in = communication.getInputStream();
StringBuilder receivedMsg = new StringBuilder();
synchronized (in) {
int c;
for (c = in.read(); in.available() != 0; c = in.read()) {
receivedMsg.append((char) c);
}
receivedMsg.append((char) c);
}
System.out.println("Norton received:" + receivedMsg.toString());
OutputStream out = communication.getOutputStream();
synchronized (out) {
out.write("Hello Qiume, this is Norton, I love you!".getBytes());
}
synchronized (in) {
receivedMsg = new StringBuilder();
int c;
for (c = in.read(); in.available() != 0; c = in.read()) {
receivedMsg.append((char) c);
}
receivedMsg.append((char) c);
}
System.out.println("Norton received:" + receivedMsg.toString());
communication.close();
System.out.println("Norton out!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
//Main.java
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable client = new Runnable() {
@Override
public void run() {
Client norton = new Client();
norton.start();
}
};
Runnable server = new Runnable() {
@Override
public void run() {
Server qiume = new Server();
qiume.serve();
}
};
new Thread(server).start();
while (true){
Thread.sleep(1000);
new Thread(client).start();
}
}
}