Java拥有强大的网络编程能力,JDK中对网络编程提供的API也大大地降低了开发者进行网络编程的难度。
计算机网络模型
在讲Java网络编程之前,我们需要复习一下计算机网络的一些基础知识。
网络模型一般是指OSI七层参考模型和TCP/IP五层参考模型。这两个模型在网络中应用最为广泛。
OSI参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI参考模型或七层模型。
TCP/IP是传输控制协议/网络协议模型(Transmission Control Protocol/Internet Protocol),相比于OSI参考模型,它少了表示层和会话层。
应用层:应用层是最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务,其功能是直接向用户提供服务,完成用户希望在网络上完成的各种工作。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,TELNET等。
表示层:是OSI模型的第六层,它对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是“处理用户信息的表示问题,如编码、数据格式转换和加密解密”等。
会话层:是OSI模型的第五层,是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。将不同实体之间的表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。
传输层:OSI下3层的主要任务是数据通信,上3层的任务是数据处理。而传输层(Transport Layer)是OSI模型的第四层。因此该层是通信子网和资源子网的接口和桥梁,起到承上启下的作用。
该层的主要任务是:向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输。传输层的作用是向高层屏蔽下层数据通信的细节,即向用户透明地传送报文。该层常见的协议:TCP/IP中的TCP协议、Novell网络中的SPX协议和微软的NetBIOS/NetBEUI协议。
网络层:是OSI模型的第三层,它是OSI参考模型中最复杂的一层,也是通信子网的最高一层。它在下两层的基础上向资源子网提供服务。其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。
数据链路层:是OSI模型的第二层,负责建立和管理节点间的链路。该层的主要功能是:通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路。
在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能是在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。
物理层:在OSI参考模型中,物理层(Physical Layer)是参考模型的最低层,也是OSI模型的第一层。物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。
物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的“比特流”没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
TCP与UDP协议
传输层是网络模型中承上启下的一层,向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输。传输层的协议主要是TCP协议和UDP协议。
TCP协议
TCP是Transmission Control Protocol的简称,是一种面向连接的保证可靠的基于字节流传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。
TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:
(1)基于流的方式;
(2)面向连接;
(3)可靠通信方式;
(4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;
(5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。
为满足TCP协议的这些特点,TCP协议做了如下的规定:
①数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
②到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
③超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
④滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只会允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
⑤失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
⑥重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据;
⑦数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段,导致对端超时并重发。
UDP协议
UDP是User Datagram Protocol的简称,是一种无连接、不可靠的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
主要特点如下:
(1)由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
(2)UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包而言UDP的额外开销很小。
(3)吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
(4)UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
TCP与UDP协议对比
(1)TCP 是面向连接的传输控制协议,而UDP 提供了无连接的数据报服务;
(2)TCP 具有高可靠性,确保传输数据的正确性,不出现丢失或乱序;
(3)UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序,应用程序需要负责传输可靠性方面的所有工作;
(4)UDP 具有较好的实时性,工作效率较 TCP 协议高;
(5)UDP 段结构比 TCP 的段结构简单,因此网络开销也小;
(6)TCP 协议可以保证接收端毫无差错地接收到发送端发出的字节流,为应用程序提供可靠的通信服务。对可靠性要求高的通信系统往往使用 TCP 传输数据。
TCP协议中的三次握手、四次挥手
建立起一个TCP连接需要先经过“三次握手”:
第一次握手,客户端发送syn包(syn = j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手,服务器收到syn包,必须确认客户的syn(ack=j+1),同时自己也发生一个syn包(syn=k),即发送syn+ack包,此时服务器进入SYN_RECV状态;
第三次握手,客户端收到服务器的syn+ack包,向服务器发送确认包ack(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
三次握手过程中不包含数据,只是为了确认客户端和服务器的发送和接收功能正常,三次握手完毕后,客户端和服务器才正式开始传送数据,理想情况下,TCP连接一旦建立,在通信双方任意一方主动关闭前,TCP连接都将一直保持下去。
注意:SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。
关闭连接需要经过“四次挥手”,刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:
1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
2、第二次挥手:服务端在收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端将处于 LAST_ACK 的状态。
4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号+ 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端已收到自己的 ACK 报文之后才会进入 CLOSED 状态。
5、服务端在收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。
IP地址与端口
IP地址是用来标志网络中的一个通信实体的地址,通信实体可以是计算机、路由器等。
计算机网络中的每一个通信实体的IP地址都是独一无二的,在进行通信是,通信实体需要知道对方的地址才能准确地将消息发送过去。
在Java中有一个IP地址地抽象类InetAddress,它继承Object类,实现了Serializable ,直接子类有Inet4Address和Inet6Address,分别对应IPv4地址和IPv6地址。
IP 地址是 IP 使用的 32 位(IPv4)或 128 位(IPv6)无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。InetAddress 的实例包含 IP 地址,还可能包含相应的主机名(取决于它是否用主机名构造或者是否已执行反向主机名解析)。
特殊的IP地址:127.0.0.1是一个特殊的IP地址,它表示本机IP地址,也叫回环地址;192.168.0.0~192.168.255.255是私有地址又称局域网地址,属于非注册地址,专门为组织机构内部使用,不能用于Internet。
例如:
package com.test;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IPTest {
public static void main(String[] args) {
//使用getLocalHost方法创建InetAddress对象
InetAddress addr = null;
try {
addr = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(addr.getHostAddress()); //返回:192.168.1.110
System.out.println(addr.getHostName()); //输出计算机名
//根据域名得到InetAddress对象
try {
addr = InetAddress.getByName("www.163.com");
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(addr.getHostAddress()); //返回 163服务器的ip:61.135.253.15
System.out.println(addr.getHostName()); //输出:www.163.com
}
}
运行结果:
注意InetAddress.getLocalHost()方法返回的地址不是127.0.0.1,而是本计算机在私有网段中的地址。
IP地址可以用来标志一台计算机,但是每台计算机将会运行多个应用程序,我们可以使用端口号来标志这些应用程序,不同应用程序同一个协议下(TCP或UDP)的端口不能冲突。
一个IP地址的端口通过16bit进行编号,最多可以有65536个端口(TCP协议有65536个,UDP协议也有65536个)。端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535。
端口号也是有限的,需要进行分配,一般公认端口范围是 0—1023,比如80端口分配给WWW,21端口分配给FTP;注册端口范围是 1024—49151 分配给用户进程或应用程序;动态/私有端口范围是 49152—65535。
在Windows下可以通过命令行来看到本机运行程序的IP及端口号:
在Java中封装了一个IP套接字地址类:InetSocketAddress类,实现 IP 套接字地址(IP 地址 + 端口号)。它还可以是一个对(主机名 + 端口号),在此情况下,将尝试解析主机名。如果解析失败,则该地址将被视为未解析 地址,但是其在某些情形下仍然可以使用,比如通过代理连接。
它提供不可变对象,供套接字用于绑定、连接或用作返回值。
例如:
package com.test;
import java.net.InetSocketAddress;
public class IPTest {
public static void main(String[] args) {
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress socketAddress2 = new InetSocketAddress("localhost",9000);
System.out.println(socketAddress.getHostName());
System.out.println(socketAddress2.getAddress());
System.out.println(socketAddress.getPort());
}
}
运行结果:
Socket
Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信的五种必须信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
套接字(socket)是一个抽象层,传输层连接的端点叫做套接字(socket)。应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
JDK中也封装了Socket,TCP协议的实现是Socket和ServerSocket,UDP协议的实现是DatagramSocket。
Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。
例如:
服务端socket示例:
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);
// server将一直等待连接的到来
System.out.println("server将一直等待连接的到来");
Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len, "UTF-8"));
}
System.out.println("get message from client: " + sb);
inputStream.close();
socket.close();
server.close();
}
}
客户端socket示例:
import java.io.OutputStream;
import java.net.Socket;
public class ClientSocketTest {
public static void main(String args[]) throws Exception {
// 要连接的服务端IP地址和端口
String host = "127.0.0.1";
int port = 55533;
// 与服务端建立连接
Socket socket = new Socket(host, port);
// 建立连接后获得输出流
OutputStream outputStream = socket.getOutputStream();
String message = "你好 yiwangzhibujian";
socket.getOutputStream().write(message.getBytes("UTF-8"));
outputStream.close();
socket.close();
}
}
运行结果:
这是一个客户端单向发送消息到服务器端的demo,且服务器端是只等待接收一次连接请求。从示例代码中可以看到创建socket需要指的IP地址和端口,发送和接收消息是需要通过输入输出流的。
Java中UDP协议的socket实现是DatagramSocket,并且不区分客户端和服务端。
例如:
服务端socket示例:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class DatagramSocketServerTest {
public static void main(String[] args) {
try {
// 要接收的报文
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
// 创建socket并指定端口
DatagramSocket socket = new DatagramSocket(8088);
System.out.println("服务器端正在等待客户端连接...");
// 接收socket客户端发送的数据。如果未收到会一致阻塞
socket.receive(packet);
String receiveMsg = new String(packet.getData(), 0, packet.getLength());
System.out.println(packet.getLength());
System.out.println(receiveMsg);
// 关闭socket
socket.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
客户端socket示例:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DatagramSocketClientTest {
public static void main(String[] args) {
try {
// 要发送的消息
String sendMsg = "客户端发送的消息";
// 获取服务器的地址
InetAddress addr = InetAddress.getByName("localhost");
// 创建packet包对象,封装要发送的包数据和服务器地址和端口号
DatagramPacket packet = new DatagramPacket(sendMsg.getBytes(), sendMsg.getBytes().length, addr, 8088);
// 创建Socket对象
DatagramSocket socket = new DatagramSocket();
// 发送消息到服务器
socket.send(packet);
// 关闭socket
socket.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
这也是一个客户端单向发送消息到服务器端的demo,且服务器端是只等待接收一次连接请求。从示例代码中可以看到使用DatagramSocket进行通信需要将消息、消息长度和对方地址、端口号封装在DatagramPacket中,DatagramSocket发送出的每一个DatagramPacket都包含了IP地址和端口号。