JAVA SOCKET 编程——TCP/UDP
- 2022 年 6 月 21 日
本文字数:5484 字
阅读完需:约 18 分钟
什么是 SOCKET?
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。简而言之,socket 是一个应用层之下,传输层之上的接口的接口层。
可以理解为去银行办理业务,我们所在的位置就是应用层,柜台里面为传输层,柜台的窗口就是 socket 接口。
那么,问题来了,如何在互联网中明确标记一台设备和唯一的一个通信通道呢?ip 地址,用来标识唯一的一台设备。端口(port)号用来标识一个进程。使用源 ip 地址+源 port 号+目标 ip 地址+目标 port 号来标识一个唯一的通信通道。
关于 TCP 和 UDP,它们都是传输层的协议,不同的是:1、TCP 是可靠的,UDP 是不可靠的。可靠并不代表数据一定能够通过网络发送成功,而是发送的数据会尽可能的发送成功,并且即使失败了,对方也有感知。2、TCP 是有连接的,而 UDP 是无连接的。3、TCP 是面向字节流的,而 UDP 是面向数据报文的。
简单了解了 TCP/UDP 协议的不同后,我们使用 Java Socket,基于 TCP/UDP 协议,实现客户端给服务端发送信息,经过处理发送回客户端,客户端进行显示。
1、基于 TCP 的 SOCKET 编程
1.1、CLIENT 端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9898);
Scanner console = new Scanner(System.in);
System.out.print("请输入请求>");
String request = console.nextLine();
OutputStream os = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
writer.println(request);
writer.flush();
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(is, "UTF-8");
String response = scanner.nextLine(); //对象没有响应,就一直等
System.out.println(response);
socket.close();
}
}
2.2、SERVER 端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
private static class ServiceMan extends Thread {
private final Socket socket;
ServiceMan(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//获取输入流
InputStream is = socket.getInputStream();
//封装成Scanner
Scanner scanner = new Scanner(is, "UTF-8");
//使用\r\n进行分割的方式,读取请求
//等着第一个Client发送请求
String request = scanner.nextLine();//nextLine把\r\n已经去掉了
System.out.println("收到请求:" + request);
//业务处理
String response = request;
//发送响应,也需要使用\r\n跟在后面,进行分割
OutputStream os = socket.getOutputStream();//得到输出流
//封装成PrintWriter
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(os, "UTF-8")
);
//发送响应
writer.println(response);//println会帮我们在后面加\r\n
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
//开店
ServerSocket serverSocket = new ServerSocket(9898);
ExecutorService threadPool = Executors.newFixedThreadPool(20);
//循环处理任务
//主线程只负责接待客人——建立连接的过程
while (true) {
/*
Socket socket = serverSocket.accept();
//所有业务处理的过程,交给工作线程去处理
new ServiceMan(socket).start();
*/
//更好的做法,引进线程池
//各个线程之间,没有数据共享(主线程和工作线程共享socket)
//所以天生是线程安全的
Socket socket = serverSocket.accept();
threadPool.execute(new ServiceMan(socket));
}
}
}
这里使用了线程池,是因为:当有多个 Client 对 Server 发送请求时:
Server 会先和第一个发送请求的 Client 建立连接,后序建立连接的所有 Client 必须等待第一个 Client 与 Server 的交互完成,才能与 Server 进行交互。如果第一个建立连接的 Client 一直没有发送消息,那么即使后面建立连接的 Client 向 Server 发送消息,Server 也接收不到,就会进入一个阻塞状态。多线程的好处就是能够处理这种阻塞状态,并且各个线程之间没有数据共享,所以天生就是线程安全的。
2、基于 UDP 的 SOCKET 编程
C/S 模型(客户端/服务端)流程图
2.1、SERVER 端
1、创建server的socket
2、循环读取请求(request),解析并处理请求,生成响应(response)
3、发送响应
提供给Server一些简单的功能。
version1:回显服务。
发送给服务端什么,就把这个消息发送回去。
version2:翻译服务。
输入英文,返回对应的意思和例句。
version3:轮盘聊天。
多个客户端,其中一个客户端发送消息到服务端,服务端随机将这个消息返回给某一个客户端。
1234567891011
代码如下:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.*;
public class Server {//服务端
public static void main(String[] args) throws IOException {
//1.创建server的socket 类似于开饭店
// 内部会进行本地ip+port的绑定
// 例子:饭店开张,提供一个大家都认识的地段 ip + port
// ip虽然没传,但内部会帮我们处理,把所有的ip都会绑定
try (DatagramSocket socket = new DatagramSocket(9939)) {
//2.开门迎客,通过循环,处理业务
while (true) {
//3.处理一个要求并返回响应
action(socket);
}
}
}
/**
* 处理要求
*/
private static void action(DatagramSocket socket) throws IOException {
//1.读取客户端发来的请求
//1.1准备一个字节数组,用来存放一会儿要读到的数据
byte[] receiveBuffer = new byte[8192];
//1.2把buffer封装成datagram
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, 0, 8192);
//1.3读取请求
socket.receive(receivePacket);
//1.4从receive中返回,就意味着,有人给我发送请求了
//需要将byte[]中的数据进行 字符集编码->String
String request = new String(receiveBuffer, 0, receivePacket.getLength(), "UTF-8");
System.out.printf("收到的请求是|%s|%n", request);//收到请求
//2.进行服务---根据请求,处理业务,并生成响应
//Version1:回显服务——echo服务
//客户端发送什么过来,就发送回去什么
//String response=request;
//Version2:字典查询服务——请求是英文,响应是中文+例句 有道查询
String response = translate(request);
//Version3:轮盘聊天——给我发送过请求的ip+port,我会记录下来,
// 然后再有人给我发送来新的请求时,随机选择一个ip+port发送回去
//不保证对方还在线,所以不保证对方能收到
//randomTalk(socket,request, receivePacket.getAddress(), receivePacket.getPort());
//3.发送响应回去
//Version1、2对应的发送响应代码块
byte[] sendBuffer = response.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, 0,
sendBuffer.length,
receivePacket.getAddress(),
receivePacket.getPort());
socket.send(sendPacket);
}
/*Version3对应的发送响应代码块
private static class Remote {//Remote用来保存客户端的ip+port
private InetAddress address;
private int port;
private Remote(InetAddress address, int port) {
this.address = address;
this.port = port;
}
}
//所有曾经给我发消息的客户端的信息——远端
private static List<Remote> remoteList = new ArrayList<>();
private static Random random = new Random();
private static void randomTalk(DatagramSocket socket,String request, InetAddress address, int port) throws IOException {
System.out.printf("之前已经有%s个客户端发送了消息%n",remoteList.size());
if(remoteList.size()>0) {
//随机一个下标,决定吧这个消息发给他
int rIndex = random.nextInt(remoteList.size());
Remote remote = remoteList.get(rIndex);
System.out.printf("决定发送给%s客户端%n",rIndex);
//发送消息
byte[] sendBuffer = request.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, 0,
sendBuffer.length,
remote.address,
remote.port
);
socket.send(sendPacket);
}
//发送完毕将自己加入remoteList中
remoteList.add(new Remote(address,port));
}
*/
//Version2用到的内部类和一些静态代码块、方法
private static class Result {//字典
String chinese;//中文字段
String sentence;//英文字段
private Result(String chinese, String sentence) {
this.chinese = chinese;
this.sentence = sentence;
}
}
private static Map<String, Result> dictionary = new TreeMap<>();//用来存放字典中的元素
//静态代码块,用于初始化静态属性
static {
dictionary.put("dictionary", new Result("字典", "He threw my dictionary back."));
dictionary.put("mask", new Result("口罩", "They contrived a mask again"));
}
private static String translate(String english) {
//按最简单的翻译功能实现——提前保存一份字典
Result result = dictionary.get(english);
if (result == null) {
return "不支持的单词.";
}
return String.format("%s%n%s%n", result.chinese, result.sentence);
}
}
2.2、CLIENT 端
循环{
1、读取用户输入
2、封装成请求并发送
3、读取并解析响应
4、把结果返回给用户
}
代码如下:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Client {//客户端
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
try (DatagramSocket socket = new DatagramSocket()) {
while (true) {
//读取用户输入
System.out.print("随便输入什么然后回车>");
String str = scanner.nextLine();
//发送请求
byte[] sendBuffer = str.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(
sendBuffer, 0, sendBuffer.length,
InetAddress.getByName("127.0.0.1"), 9939
);
socket.send(sendPacket);//完成发送
//接收响应的过程
byte[] receiveBuffer = new byte[8192];
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, 0, receiveBuffer.length);
socket.receive(receivePacket);
//真正接收到响应,进行字符集解码处理
String response = new String(receiveBuffer, 0, receivePacket.getLength(),"UTF-8");
System.out.printf("From 服务端$|%s|%n", response);
}
}
}
}
版权声明: 本文为 InfoQ 作者【乌龟哥哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/cf33fd2f1b99fdc97f884ca40】。文章转载请联系作者。
乌龟哥哥
正在努力寻找offer的大四小菜鸟 2021.03.16 加入
擅长 Hbuilder、VS Code、MyEclipse、AppServ、PS 等软件的安装与卸载 精通 Html、CSS、JavaScript、jQuery、Java 等单词的拼写 熟悉 Windows、Linux、 等系统的开关机 看–时间过得多快,不说了,去搬砖了
评论