摘要:读完本章您将对Java网络编程有一定的了解,知道UDP与TCP的区别,会用Java实现UDP、TCP传输数据。
一、什么是UDP、TCP。
网络编程顾名思义就是利用编程语言实现不同终端之间的通信,这其中包括发送端(客户端)通过规定好的协议组装包,在接收端(服务端)将包进行解析,从而提取出对应的信息,实现通信的目的。这里的协议主要有UDP与TCP协议,对应不同的协议,Java有不同的操作类实现,在Java中实现网络编程的类在java.net包下。
UDP:UDP是User Datagram Protocol的简称,中文名为用户数据报协议。它是一种面向无连接的传输协议,何为面向无连接,就是在传输数据的时候,不需要判断是否与服务端建立连接就可发送数据,对方是否成功接收并不知情。这样就导致在传输的时候安全性较差,无法保证数据准确抵达,但在传输数据的效率上会稍微快一点。
TCP:TCP是Transmission Control Protocol的简称,中文名为传输控制协议。它是一种面向连接的、可靠的、基于字节流的传输层通信协议。何为面向连接,就是在传输数据之前,客户端需要与服务端建立连接,连接成功后才能传输数据进而实现通信目的。TCP建立连接遵循三次握手协议,即客户端向服务端发起SYN请求(第一次握手),服务端收到SYN请求并向客户端回应一个ACK+SYN给客户端(第二次握手),客户端收到服务端的SYN报文并回应一个ACK(第三次握手)连接成功,这时可以开始传输数据了。相比于UDP协议,TCP协议较安全可靠,效率上稍微慢一点点。
二、Java UDP编程
java udp涉及到的类主要有DatagramSocket、DatagramPacket,浏览两个类的API可知:
DatagramSocket:
它的主要构造方法有:
DatagramSocket()的主要方法有:
public synchronized void receive(DatagramPacket p) throws IOException{...}
从此套接字接收数据包,数据报包DatapramPacket 缓冲区填充了接收的数据,数据报包还包含发送方的IP地址和发送机器上的端口号。此方法在未收到数据报包前会一直阻塞。数据报包对象的length字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
public void send(DatagramPacket p) throws IOException {...}
从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
DatagramPacket :
此类表示数据报包。数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
其主要构造方法有:
主要方法有:
一个UDP Java Demo:
接收端:
// 创建接收端Socket, 绑定本机IP地址, 绑定指定端口
DatagramSocket socket = new DatagramSocket(6789);
// 创建接收端Packet, 用来接收数据
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
// 用Socket接收Packet, 未收到数据时会阻塞
socket.receive(packet);
// 关闭Socket
socket.close();
// 从Packet中获取数据
byte[] data = packet.getData();
int len = packet.getLength();
String s = new String(data, 0, len, "UTF-8");
System.out.println(s);
发送端:
String s = "测试UDP!";
// 创建发送端Socket, 绑定本机IP地址, 绑定任意一个未使用的端口号
DatagramSocket socket = new DatagramSocket();
// 创建发送端Packet, 指定数据, 长度, 地址, 端口号
DatagramPacket packet = new DatagramPacket(s.getBytes("UTF-8"), s.getBytes().length, InetAddress.getByName("127.0.0.1"), 6789);
// 使用Socket发送Packet
socket.send(packet);
// 关闭Socket
socket.close();
三、TCP
Java实现TCP数据传输涉及到的类有Socket、ServerSocket。由此可看出TCP分客户端服务端,而UDP不分客户端服务端。Socket客户端服务端的读写是有先后顺序的,建立连接之后,客户端需要先向服务端写数据,写完之后需要调用socket.shutdownOutput()方法告诉服务端已经写完,这样服务端read()才会返回-1,客户端写完数据再读服务端返回的数据;而服务端是先读客户端传输过来的数据,再写需要向客户端传输的数据。如果是客户端先读服务端返回的数据再写向服务端发送的数据,这时服务端始终会先执行读客户端传输过来的数据,这时客户端却还没写,读的数据就会是空的。下面用代码演示:
1、服务端先写再读,客户端先读再写,服务端读的数据是空的。
服务端代码:
System.out.println("服务端控制台输出");
// 创建服务端serverSocket
ServerSocket serverSocket = new ServerSocket(8091);
// 服务端等待连接,如果未收到请求将一直阻塞
Socket socket = serverSocket.accept();
// 从socket中获取输出流,向客户端写数据
OutputStream outputStream = socket.getOutputStream();
// 从socket中获取输入流,读取客户端写的数据
InputStream inputStream = socket.getInputStream();
String s = "服务端返回给客户端的数据,服务端返回数据时间:" + System.nanoTime();
// 服务端先写,往客户端返回数据
outputStream.write(s.getBytes());
socket.shutdownInput();
// 服务端后读客户端传输过来的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
System.out.println("服务端读数据");
sb.append(new String(buf,0,len));
}
//传输过来的数据为空
System.out.println("在时间点为" + System.nanoTime() + "时服务端接收客户端的数据是【"+sb+"】");
inputStream.close();
serverSocket.close();
客户端代码:
System.out.println("客户端控制台输出");
// 创建一个未连接的socket连接
Socket socket = new Socket();
SocketAddress sa = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8091);
// 建立连接
socket.connect(sa);
// 创建一个连接到端口为8091、地址为127.0.0.1的连接,如果服务端未开启会抛出异常
//socket = new Socket("127.0.0.1", 8091);
// 从socket中获取输入流,读取服务端返回的数据
InputStream inputStream = socket.getInputStream();
// 从socket中获取输出流,向服务端写数据
OutputStream outputStream = socket.getOutputStream();
// 客户端先读服务端返回的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
sb.append(new String(buf,0,len));
}
System.out.println("在时间点为" + System.nanoTime() + "时客户端读到服务端返回的数据【"+sb+"】");
// 客户端后向服务端写传输的数据
String s = "我是客户端发来的数据"+ System.nanoTime();
System.out.println(s);
outputStream.write(s.getBytes());
socket.shutdownOutput();
socket.close();
System.out.println("客户端已关闭");
执行后控制台输出:
服务端控制台输出
在时间点为1970625548654245时服务端接收客户端的数据是【】
客户端控制台输出
在时间点为1970625549359944时客户端读到服务端返回的数据【服务端返回给客户端的数据,服务端返回数据时间:1970625548025315】
我是客户端发来的数据1970625549474071
客户端已关闭
// 服务端返回数据时间: 1970625548025315
// 客户端读到数据时间 1970625549359944
// 客户端写数据时间 1970625549474071
// 服务端读客户端数据的时间 1970625548654245
为了更好的说明字符串内容都加了个时间戳,由控制台打印内容可看出,服务端并没有读到客户端发过来的数据,由时间可知,服务端读的时候,客户端还没向服务端写完。
2、客户端先写再读,服务端先读再写
客户端代码:
System.out.println("客户端控制台输出");
// 创建一个未连接的socket连接
Socket socket = new Socket();
SocketAddress sa = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8091);
// 建立连接
socket.connect(sa);
// 创建一个连接到端口为8091、地址为127.0.0.1的连接,如果服务端未开启会抛出异常
//socket = new Socket("127.0.0.1", 8091);
// 从socket中获取输入流,读取服务端返回的数据
InputStream inputStream = socket.getInputStream();
// 从socket中获取输出流,向服务端写数据
OutputStream outputStream = socket.getOutputStream();
// 客户端向服务端写数据
String s = "我是客户端发来的数据"+ System.nanoTime();
outputStream.write(s.getBytes());
// 必须要关闭socket的输出流,告诉对方已经写完,对方用read读才能读到-1,
// 如果用outputStream.close()会关掉整个socket连接,后续不能再操作
socket.shutdownOutput();
// 客户端读服务端返回的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
sb.append(new String(buf,0,len));
}
System.out.println("在时间点为" + System.nanoTime() + "时客户端读到服务端返回的数据【"+sb+"】");
socket.close();
System.out.println("客户端已关闭");
服务端代码:
System.out.println("服务端控制台输出");
// 创建服务端serverSocket
ServerSocket serverSocket = new ServerSocket(8091);
// 服务端等待连接,如果未收到请求将一直阻塞
Socket socket = serverSocket.accept();
// 从socket中获取输出流,向客户端写数据
OutputStream outputStream = socket.getOutputStream();
// 从socket中获取输入流,读取客户端写的数据
InputStream inputStream = socket.getInputStream();
// 服务端读客户端传输过来的数据
byte[] buf = new byte[1024];
int len=0;
StringBuffer sb = new StringBuffer();
while((len=inputStream.read(buf))!=-1){
sb.append(new String(buf,0,len));
}
System.out.println("在时间点为" + System.nanoTime() + "时服务端接收客户端的数据是【"+sb+"】");
String s = "服务端返回给客户端的数据,服务端返回数据时间:" + System.nanoTime();
// 服务端向客户端写数据
outputStream.write(s.getBytes());
// 写完之后必须调用outputStream.close()或者shutdownOutput()
//socket.shutdownOutput();
outputStream.close();
serverSocket.close();
控制台输出:
服务端控制台输出
在时间点为1971871977863049时服务端接收客户端的数据是【我是客户端发来的数据1971871977069907】
客户端控制台输出
在时间点为1971871978901276时客户端读到服务端返回的数据【服务端返回给客户端的数据,服务端返回数据时间:1971871978031776】
客户端已关闭
结论:由输出结果可知,服务端收到了客户端发来的数据,客户端也收到了服务端返回的数据。
注意:写数据之后只有关闭输出流了,对方用read()方法读才有可能返回-1,如若未关闭,读方法一直会被阻塞,直到超时。在socket中关闭流有两种方式
socket.shutdownOutput()
outputStream.close()
二者的区别就是前者是半关闭,后者是将对应的socket连接也关闭了。
对于Socket还有其他知识点,比如NIO,下章将会详细介绍,要想熟悉NIO,Socket必须要掌握,本章只是简单入门。