刚给大家讲解Java socket通信后,好多童鞋私信我,有好多地方不理解,看不明白。特抽时间整理一下,详细讲述Java socket通信原理和实现案例。整个过程楼主都是通过先简单明了的示例让大家了解整个基本原理,后慢慢接近生产实用示例,先概况后脉络给大家梳理出来的,所有涉及示例都可以直接拷贝运行。楼主才疏学浅,如有部分原理错误请大家及时指正,或发邮件与楼主交流:mail:twisttime_8633@aliyun.com. QQ:125717901。
请尊重作者劳动成果,转载请标明原文链接:https://www.jianshu.com/p/cde27461c226
整理和总结了一下大家常遇到的问题:
1. 客户端socket发送消息后,为什么服务端socket没有收到?
2. 使用while 循环实现连续输入,是不是就是多线程模式?
3. 对多线程处理机制不是很明白,希望详细讲解?
4. 希望详细讲解ServerSocketChannel和SocketChannel与ServerSoket和Socket的区别?
5. 希望有详细的例子,可以直接拷贝下来运行?
针对童鞋们提出的问题,我会在本文章中详细一一简答,并且给出详细的例子,下面言归正传。
一:socket通信基本原理。
首先socket 通信是基于TCP/IP 网络层上的一种传送方式,我们通常把TCP和UDP称为传输层。
如上图,在七个层级关系中,我们将的socket属于传输层,其中UDP是一种面向无连接的传输层协议。UDP不关心对端是否真正收到了传送过去的数据。如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。
socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,对用户来说,只要通过一组简单的API就可以实现网络的连接。借用网络上一组socket通信图给大家进行详细讲解:
首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。
二:socket通信基本示例:
在对socket通信基本原理明白后,那我们就写一个最简单的示例,展示童鞋们常遇到的第一个问题:客户端发送消息后,服务端无法收到消息。
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket =new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读取一行数据
String str = bufferedReader.readLine();
//输出打印
System.out.println(str);
}catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package socket.socket1.socket;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket =new Socket("127.0.0.1",9999);
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str="你好,这是我的第一个socket";
bufferedWriter.write(str);
}catch (IOException e) {
e.printStackTrace();
}
}
}
启动服务端:
发现正常,等待客户端的的连接
启动客户端:
发现客户端启动正常后,马上执行完后关闭。同时服务端控制台报错:
服务端控制台报错:
然后好多童鞋,就拷贝这个java.net.SocketException: Connection reset上王查异常,查询解决方案,搞了半天都不知道怎么回事。解决这个问题我们首先要明白,socket通信是阻塞的,他会在以下几个地方进行阻塞。第一个是accept方法,调用这个方法后,服务端一直阻塞在哪里,直到有客户端连接进来。第二个是read方法,调用read方法也会进行阻塞。通过上面的示例我们可以发现,该问题发生在read方法中。有朋友说是Client没有发送成功,其实不是的,我们可以通debug跟踪一下,发现客户端发送了,并且没有问题。而是发生在服务端中,当服务端调用read方法后,他一直阻塞在哪里,因为客户端没有给他一个标识,告诉是否消息发送完成,所以服务端还在一直等待接受客户端的数据,结果客户端此时已经关闭了,就是在服务端报错:java.net.SocketException: Connection reset
那么理解上面的原理后,我们就能明白,客户端发送完消息后,需要给服务端一个标识,告诉服务端,我已经发送完成了,服务端就可以将接受的消息打印出来。
通常大家会用以下方法进行进行结束:
socket.close() 或者调用socket.shutdownOutput();方法。调用这俩个方法,都会结束客户端socket。但是有本质的区别。socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。而socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。现在我们将上面的客户端示例修改一下啊,增加一个标识告诉流已经输出完毕:
客户端2:
package socket.socket1.socket;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket =new Socket("127.0.0.1",9999);
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str="你好,这是我的第一个socket";
bufferedWriter.write(str);
//刷新输入流
bufferedWriter.flush();
//关闭socket的输出流
socket.shutdownOutput();
}catch (IOException e) {
e.printStackTrace();
}
}
}
在看服务端控制台:
服务端在接受到客户端关闭流的信息后,知道信息输入已经完毕,苏哦有就能正常读取到客户端传过来的数据。通过上面示例,我们可以基本了解socket通信原理,掌握了一些socket通信的基本api和方法,实际应用中,都是通过此处进行实现变通的。
三:while循环连续接受客户端信息:
上面的示例中scoket客户端和服务端固然可以通信,但是客户端每次发送信息后socket就需要关闭,下次如果需要发送信息,需要socket从新启动,这显然是无法适应生产环境的需要。比如在我们是实际应用中QQ,如果每次发送一条信息,就需要重新登陆QQ,我估计这程序不是给人设计的,那么如何让服务可以连续给服务端发送消息?下面我们通过while循环进行简单展示:
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket =new ServerSocket(9999);
//等待客户端的连接
Socket socket = serverSocket.accept();
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine())!=null){
//输出打印
System.out.println(str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket =new Socket("127.0.0.1",9999);
//通过socket获取字符流
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while (true){
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
客户端控制中心:
服务端控制中心:
大家可以看到,通过一个while 循环,就可以实现客户端不间断的通过标准输入流读取来的消息,发送给服务端。在这里有个细节,大家看到没有,我客户端没有写socket.close() 或者调用socket.shutdownOutput();服务端是如何知道客户端已经输入完成了?服务端接受数据的时候是如何判断客户端已经输入完成呢?这就是一个核心点,双方约定一个标识,当客户端发送一个标识给服务端时,表明客户端端已经完成一个数据的载入。而服务端在结束数据的时候,也通过这个标识进行判断,如果接受到这个标识,表明数据已经传入完成,那么服务端就可以将数据度入后显示出来。
在上面的示例中,客户端端在循环发送数据时候,每发送一行,添加一个换行标识“\n”标识,在告诉服务端我数据已经发送完成了。而服务端在读取客户数据时,通过while ((str = bufferedReader.readLine())!=null)去判断是否读到了流的结尾,负责服务端将会一直阻塞在哪里,等待客户端的输入。
通过while方式,我们可以实现多个客户端和服务端进行聊天。但是,下面敲黑板,划重点。由于socket通信是阻塞式的,假设我现在有A和B俩个客户端同时连接到服务端的上,当客户端A发送信息给服务端后,那么服务端将一直阻塞在A的客户端上,不同的通过while循环从A客户端读取信息,此时如果B给服务端发送信息时,将进入阻塞队列,直到A客户端发送完毕,并且退出后,B才可以和服务端进行通信。简单地说,我们现在实现的功能,虽然可以让客户端不间断的和服务端进行通信,与其说是一对一的功能,因为只有当客户端A关闭后,客户端B才可以真正和服务端进行通信,这显然不是我们想要的。 下面我们通过多线程的方式给大家实现正常人类的思维。
四:多线程下socket编程
服务端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args)throws IOException {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket =new ServerSocket(9999);
while (true){
//等待客户端的连接
Socket socket = serverSocket.accept();
//每当有一个客户端连接进来后,就启动一个单独的线程进行处理
new Thread(new Runnable() {
@Override
public void run() {
//获取输入流,并且指定统一的编码格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine())!=null){
//输出打印
System.out.println("客户端说:"+str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
客户端:
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一个socket
Socket socket =new Socket("127.0.0.1",9999);
//通过socket获取字符流
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通过标准输入流获取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while (true){
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
通过客户端A控制台输入:
通过客户端B控制台输入:
服务端控制台:
通过这里我们可以发现,客户端A和客户端B同时连接到服务端后,都可以和服务端进行通信,也不会出现前面讲到使用while(true)时候客户端A连接时客户端B不能与服务端进行交互的情况。在这里我们看到,主要是通过服务端的 new Thread(new Runnable() {}实现的,每一个客户端连接进来后,服务端都会单独起个一线程,与客户端进行数据交互,这样就保证了每个客户端处理的数据是单独的,不会出现相互阻塞的情况,这样就基本是实现了QQ程序的基本聊天原理。
但是实际生产环境中,这种写法对于客户端连接少的的情况下是没有问题,但是如果有大批量的客户端连接进行,那我们服务端估计就要歇菜了。假如有上万个socket连接进来,服务端就是新建这么多进程,反正楼主是不敢想,而且socket 的回收机制又不是很及时,这么多线程被new 出来,就发送一句话,然后就没有然后了,导致服务端被大量的无用线程暂用,对性能是非常大的消耗,在实际生产过程中,我们可以通过线程池技术,保证线程的复用,下面请看改良后的服务端程序。
改良后的服务端:
package socket.socket1.socket;
import java.beans.Encoder;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerSocketTest {
public static void main(String[] args)throws IOException {
// 初始化服务端socket并且绑定9999端口
ServerSocket serverSocket =new ServerSocket(9999);
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
while (true) {
//等待客户端的连接
Socket socket = serverSocket.accept();
Runnable runnable = () -> {
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//读取一行数据
String str;
//通过while循环不断读取信息,
while ((str = bufferedReader.readLine()) !=null) {
//输出打印
System.out.println("客户端说:" + str);
}
}catch (IOException e) {
e.printStackTrace();
}
};
executorService.submit(runnable);
}
}
}
运行后服务端控制台:
通过线程池技术,我们可以实现线程的复用。其实在这里executorService.submit在并发时,如果要求当前执行完毕的线程有返回结果时,这里面有一个大坑,在这里我就不一一详细说明,具体我在我的另一篇文章中《把多线程说个透》里面详细介绍。本章主要讲述socket相关内容。
在实际应用中,socket发送的数据并不是按照一行一行发送的,比如我们常见的报文,那么我们就不能要求每发送一次数据,都在增加一个“\n”标识,这是及其不专业的,在实际应用中,通过是采用数据长度+类型+数据的方式,在我们常接触的热Redis就是采用这种方式,
五:socket 指定长度发送数据
清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
六:socket 建立长连接
清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
七:非阻塞ServerSocketChannel通信
清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
八:socket服务端接受信息后反馈给客户端
清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
九:socket经典小例子
清明后跟新。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
由于节前工作比较繁忙,节后会将后续章节进行详细跟新。
网友评论