俗世游子:专注技术研究的程序猿
说到前面的话
没有实战案例的理论基础都是在耍流氓,所以今天主要是想通过这里的案例能够让大家加深对之前的理解
本节我们会一步步实现一个点对点聊天小程序
Java中的Socket实现
InetAddress
InetAddress
是Java对IP地址的封装,这个类是一个基础类,下面的
ServerSocket
和
DatagramSocket
都离不开这个类
InetAddress
无法通过
new
的方式来初始化,只能提供过其提供的静态方法来调用:
// 获取本地地址InetAddress localHost = InetAddress.getLocalHost();
这里是
InetAddress
的一些方法:
// 主机名:DESKTOP-ATG4KKESystem.out.println(\"主机名:\" + localHost.getHostName());// IP地址:192.168.87.1System.out.println(\"IP地址:\" + localHost.getHostAddress());// 是否正常:trueSystem.out.println(\"是否正常:\" + localHost.isReachable(5000));
这里是我测试时的输出,
关于
isReachable()
的方法,用来检测该地址是否可以访问,由此我们可以做一些健康检查操作,比如:
// 通过主机IP或者域名来得到InetAddress对象InetAddress inetAddress = InetAddress.getByName(\"192.168.87.139\");System.out.println(\"是否正常:\" + inetAddress.isReachable(5000));
在5s之内尽最大可能尝试连接到主机,如果没有就认为主机不可用,这里受限于防火墙和服务器配置
当然,做健康检查这种方法还是low了点,生产环境中肯定不会这么干
PS: 生产环境的网络操作不会使用到这节里的东西,大部分情况下采用的都是Netty
ServerSocket
ServerSocket
是服务端套接字,是基于
TCP/IP
协议下的实现
初始化
通常我们这样来构建:
ServerSocket serverSocket = new ServerSocket(9999);ServerSocket serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(9999));
这样就完成了服务端的初始化,并且将端口
9999
绑定起来
等待连接
如果客户端想要和
ServerSocket
建立连接,我们需要这么做
for(;;) {Socket socket = serverSocket.accpet();// Socket[addr=/0:0:0:0:0:0:0:1,port=62445,localport=9999]System.out.println(socket);}
accpet()
是侦听与
ServerSocket
建立的连接,这个方法是一个阻塞方法,会一直等待连接接入进来
如果有连接接入进来,我们可以通过返回值来得到当前接入进来的
Socket
通信
在网络中传递数据其实也是按照
IO
流的方式进行传递的,但是我们只能获取到字节流:
InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream();
InputStream
读取数据,
OutputStream
写出数据,这些基本操作我们在之前的IO流中都介绍过,这里就不再多说
这里我们为了能够提高效率,可以采用
包装流
或者
处理流
来处理,这前面也介绍过了
完整小例子
其实到这里,
ServerSocket
的关键介绍也就完了,下面我们来做一个小例子:
- 当有客户端连接进来之后,给客户端返回:
Hello World
public class _ServerSocket {// 用来存储请求客户端和Socket之间的对应关系static Map<String, Socket> MAP = new HashMap<>();public static void main(String[] args) {try {ServerSocket serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(9999));for (; ; ) {String token = UUID.randomUUID().toString().replace(\"-\", \"\").toLowerCase();Socket socket = serverSocket.accept();// 对应MAP.put(token, socket);outHtml(socket);}} catch (IOException e) {e.printStackTrace();}}public static void outHtml(Socket socket) {OutputStream outputStream = null;try {outputStream = socket.getOutputStream();outputStream.write((\"HTTP/1.1 200 OK\\n\\nHello World\").getBytes(\"UTF-8\"));} catch (IOException e) {e.printStackTrace();} finally {if (null != outputStream) {try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}}}}
HTTP/1.1 200 OK\\n\\nHello World\\n
这是HTTP协议下返回类型,前面是Response固定格式,
Hello World是真正返回的内容,这样我们的
ServerSocket就能够通过浏览器来访问了
Socket
Socket
属于客户端套接字,只有先和服务端套接字建立连接才能做其他的操作,
Socket
的使用方式非常简单
建立连接
Socket socket = new Socket(\"127.0.0.1\", 9999);// 验证是否连接成功if (socket.isConnected()) {System.out.println(\"到服务端连接成功\");}
这是其中一种构造方法,更多情况下是采用这种方式
和服务端的连接建立成功之后,后续的操作就和
ServerSocket
的
通信步骤
一样了,这里就不再多废话了
下面用一个完整的例子来巩固一下
案例:TCP点对点聊天
服务端
public class Server {/*** 将客户端标识和socket关联起来*/private static final Map<String, Socket> SOCKET_MAP = new HashMap<>();/*** 反向关联,用来获取标识*/private static final Map<Socket, String> SOCKET_TOKEN_MAP = new HashMap<>();public static void main(String[] args) throws IOException {/*** 开启ServerSocket并监听9999端口*/ServerSocket serverSocket = new ServerSocket(9999);for (;;) {/*** 等待客户端连接*/Socket socket = serverSocket.accept();/*** IO读取是阻塞式方法,所以需要开启新线程,这里可以优化成线程池*/new Thread(() -> {try {saveToMap(socket);getClientMsg(socket);} catch (IOException e) {e.printStackTrace();}}).start();}}/*** 绑定SOCKET*/private static void saveToMap(Socket socket) throws IOException {String token = StringUtil.uuid();SOCKET_MAP.put(token, socket);SOCKET_TOKEN_MAP.put(socket, token);System.out.println(\"---客户端连接成功,编号:\" + token);System.out.println(\"当前用户:\" + SOCKET_MAP.size());/*** 因为没有登录,所以这里要告知客户端自己的标识*/send(token, token, token);}/*** 获取客户端发送过来的消息,并发送出指定指定的客户端*/private static void getClientMsg(Socket socket) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));String line = \"\";while ((line = reader.readLine()) != null) {// 读取到一行以后,从这里发送出去send(socket, line);}}/*** 发送消息*/private static void send(Socket socket, String line) throws IOException {String[] s = line.split(\"#\");final String from = SOCKET_TOKEN_MAP.get(socket);send(s[0], s[1], from);}/*** 发送消息* @param token* @param msg* @param from 这里在目标客户端展示* @throws IOException*/private static void send(String token, String msg, String from) throws IOException {Socket sk = SOCKET_MAP.get(token);if (null == sk)return;String s = from + \":\" + msg;System.out.println(\"---发送给客户端:\" + s );// 字符流输出BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(sk.getOutputStream()));writer.write(s);writer.newLine();writer.flush();}}
客户端
public class Client {public static void main(String[] args) throws IOException {/*** 连接到服务端*/Socket socket = new Socket(\"127.0.0.1\", 9999);/*** 开新线程读取消息,可以优化*/new Thread(() -> {try {BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));String line = \"\";while (StringUtil.isNotBlank(line = reader.readLine())) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}).start();/*** 从控制台写入消息并发送出去*/Scanner scanner = new Scanner(System.in);while (scanner.hasNext()) {String next = scanner.next();send(next, socket);}}private static void send(String msg, Socket socket) throws IOException {BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));writer.write(msg);writer.newLine();writer.flush();}}
代码已经通过测试,注释写的也非常清楚,大家可以尝试下,按照
标识#消息
的格式就可以
点对点聊天
了。
如果想要
群聊
:
- 将Socket保存到集合中,然后循环集合就可以了,非常简单
好久没有用
Socket写聊天程序了,差点就放弃了
下次改用
Netty来写,
Netty比
Socket方便多了
DatagramSocket
DatagramSocket
是用于发送和接收数据报包的套接字,是基于
UDP协议
下的实现。根据类中官方介绍:
数据报套接字是数据包传递服务的发送或接收点。 在数据报套接字上发送或接收的每个数据包都经过单独寻址和路由。 从一台机器发送到另一台机器的多个数据包可能会以不同的方式路由,并且可能以任何顺序到达
我们也能明白
UDP协议
的特性。
DatagramPacket
该类表示
数据报包
,在
DatagramSocket
中传递和接收数据都是靠这个类来完成的,比如:
- 接收数据
byte[] buffer = new byte[1024];DatagramPacket p = new DatagramPacket(buffer, buffer.length);
- 发送数据
DatagramPacket p = new DatagramPacket(\"123\".getBytes(), \"123\".getBytes().length, InetAddress.getByName(\"localhost\"), 9999);
发送数据出去,
DatagramPacket
需要指定接收端的IP和端口,这样才能够发送出去
下面我们来看看具体如何用
初始化
DatagramSocket socket = new DatagramSocket(9999);DatagramSocket s = new DatagramSocket(null);s.bind(new InetSocketAddress(9999));
两种方式都可以完成初始化,没有什么区别
接收消息
byte[] buffer = new byte[1024];DatagramPacket p = new DatagramPacket(buffer, buffer.length);socket.receive(p);System.out.println(new String(p.getData(), 0, p.getLength()));
根据
DatagramPacket
的接收参数,构造出来一个
byte[]
,然后调用
receive()
,这样消息就接收到了
receive()
是一个阻塞方法,只有等有消息的时候才会继续执行
发送消息
DatagramPacket p = new DatagramPacket(\"123\".getBytes(), \"123\".getBytes().length, InetAddress.getByName(\"localhost\"), 9999);socket.send(p);
构造发送数据包,然后调用
send()
方法就可以完成数据包的发送
UDP不需要连接,直接通过IP+PORT的方式就可以发送数据
案例:UDP聊天
public class _DatagramPacket {public static void main(String[] args) throws IOException {// 从命令行得到需要绑定的端口和发送数据的端口DatagramSocket datagramSocket = new DatagramSocket(Integer.parseInt(args[0]));System.out.println(\"已启动\");new Thread(() -> {byte[] buffer = new byte[1024];DatagramPacket p = new DatagramPacket(buffer, buffer.length);try {for (;;) {// 构建接收数据datagramSocket.receive(p);System.out.println(p.getPort() + \":\" + new String(buffer, 0, p.getLength()));}} catch (IOException e) {e.printStackTrace();}}).start();Scanner scanner = new Scanner(System.in);DatagramPacket p = new DatagramPacket(new byte[0], 0, new InetSocketAddress(\"127.0.0.1\", Integer.parseInt(args[1])));while (scanner.hasNext()) {String next = scanner.next();// 构建发送数据包p.setData(next.getBytes());datagramSocket.send(p);}}
有瑕疵,空格会换行,这里交给大家去修改了
最后的话
到这里,关于
Socket编程
方面的东西就聊完了,没有介绍很多的API方法,这些在用到的时候再看也是一样的。
以下是
java.net
所在的目录文档:
点击这里查看