学过计算机网络的同学都知道,TCP需要经过三次握手进行连接才能通信,再经过四次挥手才能断开连接,UDP则无需连接即可发送消息,但再多的理论也不如写代码来得直接,下面通过简单基础的socket来体现计算机网络的TCP/UDP思想。最近在学NIO(非阻塞IO,Non-Block IO),正好回顾下以前的OIO(“老的IO”,Old IO)。本文暂时只写了阻塞式的socket连接,非阻塞式的NIO还在学习中,后面有时间再补上,哈哈。
TCP
简单来说,TCP连接最大的特点就是要先连接,才能够进行通信,这样也能够保证通信数据传输过程中不会丢失。所以常说的TCP有连接、安全就是这个道理。
下面写一个小Demo,通过TCP实现信息传递和文件传输
关键注意socket.shutdownOutput();这行代码,这里考虑到客户端给服务端发送完消息后,必须要等待服务端回复一个消息,所以并不能直接使用socket.close()将socket套接字关闭,如果整个socket被关闭了,服务端就无法再返回给客户端数据了。
而shutdownOutput()方法有两个作用:
①关闭掉当前的socket的output流,这个关闭是单向的,比如客户端关闭了socket的output流,但服务端仍然可以进行output调用。
②调用close()、shutdownOutput()、shutdownInput()方法后,会立即将当前socket中的数据发送给对方,常用于表示发送方的数据写入完毕了。
综上两点,可以看得出来,其实调用shutdownOutput()方法就相当于四次挥手中主动发起的挥手。
- 客户端写完要发送的消息后发起shutdownOutput()作为第一次挥手
- 服务端收到作为第二次挥手
- 服务端写完要回复的消息后发起shutdownOutput()作为第三次挥手
- 客户端收到后就会自动关闭当前socket作为第四次挥手(所以这里客户端最后不调用socket.close()方法也可以正常关闭连接)。
编写客户端类:ClientDemo
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketAddress;
/**
* 客户端
*/
public class ClientDemo {
public static void main(String[] args){
ClientDemo clientDemo = new ClientDemo();
//发送消息测试
// clientDemo.sendMessage("你好,我是客户端");
//上传文件测试
String path = System.getProperty("user.dir")+"\\resource\\test1.jpg";
File file = new File(path);
clientDemo.uploadFile(file);
}
private final String serverAddress = "localhost";//服务器的地址 localhost
private final int serverPort = 9111;//服务器监听的端口号
/**
* 向服务端发送文本,服务端回应文本
* 双方通过socket建立连接
* 再通过socket对象的InputStream和OutputStream进行消息传递
* 消息写入输出流后就可以关闭输出流流
* 最后关闭socket的输出流
* @param message
*/
public void sendMessage(String message){
try{
//1. 根据服务端的ip和端口创建一个socket连接
Socket socket = new Socket(serverAddress,serverPort);
//2. 获取当前socket对象的输出流
OutputStream os = socket.getOutputStream();
//3. 将信息的字节写入到socket对象的输出流
// 服务端就通过该socket对象的输入流来读取数据
os.write(message.getBytes());
os.flush();
//关闭后该socket不可写入数据,此时socket并没有完全被关闭
//同时该语句也告知服务端数据传输完毕
socket.shutdownOutput();
System.out.println("消息《"+message+"》已发送到服务器");
//4. 接收服务端的回复信息
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];//缓冲字节流
int len;
while ((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);//将每次读取的1024字节写到管道流中,这样管道流就能保存完整的流信息
}
System.out.println("服务端返回的信息:"+baos.toString());
//5.关闭连接
is.close();//关闭从socket对象获取到的输入流
os.close();//关闭从socket对象获取到的输出流
socket.close();//关闭socket连接
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 向服务端上传文件
* 思路是先将文件File流写入socket的Output流中
* 然后服务端通过socket的input流读取到文件File流
*
* @param file
*/
private void uploadFile(File file){
try{
Socket socket = new Socket(serverAddress,serverPort);
//将文件读取到内存IO流中,并遍历字节将其写入到输入流
OutputStream os = socket.getOutputStream();
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[1024];
int len;
while((len=fis.read(buffer))!= -1){
os.write(buffer,0,len);
}
os.flush();
socket.shutdownOutput();//原理同上面的文本传输
System.out.println("文件已发送到服务器");
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();//管道流
byte[] buffer1 = new byte[1024];//缓冲字节流
int len1;
while ((len1 = is.read(buffer1)) != -1){
baos.write(buffer1,0,len1);//将每次读取的1024字节写到管道流中,这样管道流就能保存完整的流信息
}
System.out.println("服务端返回的信息:"+baos.toString());
//4.关闭各种连接,由内到外
os.close();
is.close();
fis.close();
baos.close();
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
编写服务端: ServerDemo
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端 - 这种模式是阻塞式socket,也就是说必须要等待前面的请求完成后,后面的请求才可以执行
*/
public class ServerDemo {
public static void main(String[] args) {
ServerDemo serverDemo = new ServerDemo();
//接收消息测试
// serverDemo.acceptMessage();
//接收文件测试
serverDemo.reviceFile();
}
private final int port = 9111;
private int count = 0;//统计信息条数
/**
* 接收消息
* 服务端通过ServerSocket对象来开启端口
* 再通过ServerSocket对象的accept()方法监听端口
*/
public void acceptMessage(){
try{
//1. 开启端口号的监听
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("服务端已启动,监听中。。。。。");
//2. 接收客服端传输的信息
while (true){//循环表示服务端不关闭,始终接收客服端的消息
//2.1 获取客户端的socket对象
Socket socket = serverSocket.accept();
//2.2 通过socket对象的输入流来读取数据
InputStream is = socket.getInputStream();
//2.3 使用管道流可以避免读取错误、如缓冲流大小不足导致字节不完整,从而使中文乱码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];//缓冲字节流
int len;
while ((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);//将每次读取的1024字节写到管道流中,这样管道流就能保存完整的流信息
}
System.out.println("收到"+socket.getRemoteSocketAddress()+"的消息:《"+baos.toString()+"》,累计收到消息条数:"+ (++count));
//3.返回给客户端一条消息
OutputStream os = socket.getOutputStream();
os.write("已收到".getBytes());
os.flush();
//关闭后该socket不可写入数据,此时socket并没有完全被关闭
//同时该语句也告知服务端数据传输完毕
socket.shutdownOutput();
//4. 关闭各种流的,服务端始终处于监听状态,所以不能关闭serverSocket
baos.close();//获取到数据后,关闭管道流
is.close();//关闭从socket对象获取的InputStream
os.close();//关闭从socket对象获取的OutputStream
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 接收文件
* 思路是获取客户端传递的文件IO字节流,将其读取并写出到指定路径
* 接收文件完成后返回给客户端提示信息
*/
public void reviceFile(){
try{
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("服务端已启动,等待接收。。。。。");
while(true){
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
//将文件写出
String directory = System.getProperty("user.dir")+"\\resource\\";
File dir = new File(directory);
if(!dir.exists()) dir.mkdir();
String filepath = directory + (System.currentTimeMillis()+"_accetp.jpg").substring(5);//完整文件路径
File file = new File(filepath);
if(!file.exists()) file.createNewFile();
FileOutputStream fos = new FileOutputStream(new File(file));
byte[] buffer = new byte[1024];//缓冲字节流
int len;
while ((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);//将每次读取的1024字节写到管道流中,这样管道流就能保存完整的流信息
}
System.out.println("收到"+socket.getRemoteSocketAddress()+"的文件,累计收到文件数:"+ (++count));
//返回客户端提示信息
OutputStream os = socket.getOutputStream();
os.write(("文件已上传到"+filepath).getBytes());
os.flush();
socket.shutdownOutput();
//3. 关闭各种连接,由内到外
is.close();
fos.close();
os.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
上面的代码实现了客户端与服务端的简单信息通信和文件传输简单示例
UDP
UDP连接的特点是不管接收方是否开启端口,都能发送数据,但接收方要接收到数据就必须要开启端口监听。所以说UDP无连接、不安全。
下面通过一个Demo实现两个客户端的通信
客户端1:ClinetDemo1
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
//客户端-1
public class ClientDemo1 {
public static void main(String[] args) {
//开启两个线程,一个负责发送消息,一个负责接收消息
new Thread(()->{
ClientDemo1 c1 = new ClientDemo1();
Scanner input = new Scanner(System.in);
while (input.hasNext()){//持续输入
c1.send(input.next());
}
}).start();
new Thread(()->{
ClientDemo1 c1 = new ClientDemo1();
//一直保持接受消息
while (true){
c1.receive();
}
}).start();
}
DatagramSocket sendSocket;//发送消息的socket连接对象
DatagramSocket receiveSocket;//接收消息的socket连接对象
DatagramPacket sendPacket;//要发送的报文数据对象
DatagramPacket receivePacket;//要接收的报文数据对象
int Myport = 9900;//自己的端口
/**
* 发送消息
* @param message
*/
public void send(String message){
try{
//1 建立socket连接
sendSocket = new DatagramSocket();
//2 获取要发送消息的目标地址和端口
InetAddress address = InetAddress.getByName("localhost");
int sendport = 8800;
//3 要发送的具体的数据报文(packet)对象,和地址、端口
sendPacket = new DatagramPacket(message.getBytes(),0,message.getBytes().length);
//创建这个连接
sendSocket.connect(address,sendport);
//4 发送包
sendSocket.send(sendPacket);
}catch (Exception e){
e.printStackTrace();
}finally {
sendSocket.close();
}
}
/**
* 接收消息
*/
public void receive(){
try{
//1 开放自己客户端的端口
receiveSocket = new DatagramSocket(Myport);
//2 要接收数据报文(packet)对象
byte[] buffer = new byte[1024];
receivePacket = new DatagramPacket(buffer,0,buffer.length);
//3 接收包
receiveSocket.receive(receivePacket);//阻塞式接收,直到收到消息结束
//打印接收的数据报文
String reMsg = new String(receivePacket.getData(),0,receivePacket.getData().length);
System.out.println(receivePacket.getSocketAddress()+":\t"+reMsg);
}catch (Exception e){
e.printStackTrace();
}finally {
receiveSocket.close();
}
}
}
客户端2:ClientDemo2
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
//客户端-2
public class ClientDemo2 {
public static void main(String[] args) {
//开启两个线程,一个负责发送消息,一个负责接收消息
new Thread(()->{
ClientDemo2 c2 = new ClientDemo2();
Scanner input = new Scanner(System.in);
while (input.hasNext()){//持续输入
c2.send(input.next());
}
}).start();
new Thread(()->{
ClientDemo2 c2 = new ClientDemo2();
//一直保持接受消息
while (true){
c2.receive();
}
}).start();
}
DatagramSocket sendSocket;//发送消息的socket连接对象
DatagramSocket receiveSocket;//接收消息的socket连接对象
DatagramPacket sendPacket;//要发送的报文数据对象
DatagramPacket receivePacket;//要接收的报文数据对象
int Myport = 8800;//自己的端口
/**
* 发送消息
* @param message
*/
public void send(String message){
try{
//1 建立socket连接
sendSocket = new DatagramSocket();
//2 获取要发送消息的目标地址和端口
InetAddress address = InetAddress.getByName("localhost");
int sendport = 9900;
//3 要发送的具体的数据报文(packet)对象,和地址、端口
sendPacket = new DatagramPacket(message.getBytes(),0,message.getBytes().length);
//创建这个连接
sendSocket.connect(address,sendport);
//4 发送包
sendSocket.send(sendPacket);
}catch (Exception e){
e.printStackTrace();
}finally {
sendSocket.close();
}
}
/**
* 接收消息
*/
public void receive(){
try{
//1 开放端口
receiveSocket = new DatagramSocket(Myport);
//2 要接收数据报文(packet)对象
byte[] buffer = new byte[1024];
receivePacket = new DatagramPacket(buffer,0,buffer.length);
//3 接收包
receiveSocket.receive(receivePacket);//阻塞式接收,直到收到消息结束
//打印接收的数据报文
String reMsg = new String(receivePacket.getData(),0,receivePacket.getData().length);
System.out.println(receivePacket.getSocketAddress()+":\t"+reMsg);
}catch (Exception e){
e.printStackTrace();
}finally {
receiveSocket.close();
}
}
}
通过分别创建两个线程,就能够在互不干扰的情况下实现双方的聊天,两个客户端除了端口号外,其余没有任何不同点。如果运行在不同的服务器,端口也也是可以相同的。
运行如下图