前言
—不少人初学java接触到的第一个就有挑战性的方面就是Socket编程,本文就对Socket编程中的聊天室进行解析
一、Socket是什么?
百度上是这么讲的:套接字就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口
二、我们能用Socket做什么?
1.网络聊天室的搭建
1.1首先我们来整理思路:
搭建一个聊天室需要服务端和客户端两套程序,用于进行信息的交互,其中服务端是只开启一个的,客户端是需要供多人同时使用的
1.2服务端Server:
一个java程序无非两部分构成,成员变量和方法,作为服务端我们首先需要
(1)一个私有成员变量ServerSocket用来存放服务端的Socket信息,ServerSocket与Socket不同,ServerSocket是专门用来等待客户端的连接的,一旦获得一个连接就创建一个Socket实例来与客户端进行通讯
(2)一个装PrintWriter类型的集合,用来记录需要转发每一条信息给每一个客户端的输出流,实现群聊功能
接下来是方法这些
1.构造方法
public Server(){
try {
/**
* 实例化ServerSocket的同时需要指定打开的服务端口,客户端就是通过
* 端口建立连接的
* 如果改端口已经被其他程序占用了,那么这里就会抛出异常
* java.net.BindException:address already in use
* 绑定异常
*/
System.out.println("正在启动服务端...");
server=new ServerSocket(8088);
System.out.println("服务端启动成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
2.启动方法start()
当服务端启动后便应该开始接收来自客户端的连接
public void start(){
try {
int ClientNum=0;//记录连接的客户端数量
while(true){
System.out.println("等待客户端连接中...");
Socket socket = server.accept();//这里完成对socket的赋值
System.out.println("一个客户端接入!");
ClientNum++;
Runnable handler=new ClientHandler(socket,ClientNum);//将每一个客户端的连接处理分发给一个线程
Thread t=new Thread(handler);
t.start();//这个是线程的启动方法,并不是本方法,不要混淆
}
} catch (IOException e) {
e.printStackTrace();
}
}
3.处理线程
首先在线程方法中我们需要获取本次连接的Socket,通过Socket获取输入流以及创建一个输出流,对信息进行接收、解码、阅读、写信息、编码、发送的工作
private class ClientHandler implements Runnable{
private Socket socket;
private String host;//客户端地址信息
/**外部需要传进来一个值,通过写构造器来将这个值传入方法中的成员变量,就可以直接用了*/
public ClientHandler(Socket socket,int ClientNum){
this.socket=socket;
//通过socket获取远端计算机的地址信息
host=socket.getInetAddress().getHostAddress()+"#"+ClientNum;//因为我在实验时手头只有一台电脑,为了区分客户端便为其加上了序号
}
public void run(){//继承Runnable即为线程处理类型方法,都需要重写run方法
PrintWriter pw=null;
try {
//通过Socket方法获取一个字节输入流,可以读取来自远端的信息
InputStream in=socket.getInputStream();
InputStreamReader isr=new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br=new BufferedReader(isr);
/** 输出流 给客户端发信息*/
OutputStream out=socket.getOutputStream();//字节流读取
OutputStreamWriter osw=new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw=new BufferedWriter(osw);
pw=new PrintWriter(bw,true);
//将该客户端的输出流存入共享数组allOut中,并不是字符聊天内容
/**
* 抢谁锁谁没错(临界资源),但避免对锁的东西在同步块中操作(更改),
* 在此处就不能锁allOut数组
* 在这里因为该同步块中有对该数组的扩容操作,会导致allOut对象发生变化
*/
synchronized (Server.this) {//内部类想访问外部类,外部类点this,或者Server.class
//1、对allOut进行扩容
allout.add(pw);
//allOut = Arrays.copyOf(allOut,allOut.length+1);//存在并发安全问题,当然是对于大量级来说的
//2、将pw存入共享数组的最后一个位置
//allOut[allOut.length-1]=pw;//这儿就是跑一遍,把每个客户端的端口输出流放进去就好了
}
sendMessage(host+"上线了,当前在线人数:"+ allout.size());
System.out.println(host+"上线了,当前在线人数:"+ allout.size());//给服务器端看的
String line =br.readLine();
System.out.println(host+"说:"+line);
//将消息回复给客户端
while (true){
long time0=System.currentTimeMillis();
line =br.readLine();
long time1=System.currentTimeMillis();
if(time1-time0<=500){
sendMessage("你好快啊~歇一歇吧");
continue;
}
if(line.equalsIgnoreCase("exit")){
System.out.println("客户端"+host+"主动选择退出!!");
break;
}
//服务端显示
System.out.println(host+":"+line);
//客户端显示
sendMessage(host+"说:"+line);//抽方法出去
}
} catch (IOException e) {
e.printStackTrace();
}finally{
//将当前客户端的输出流从allOut数组中删除
synchronized (Server.class) {//缩数组也需要注意上锁
allout.remove(pw);
}
sendMessage(host+"下线了,当前在线人数:"+ allout.size());
System.out.println(host+"下线了,当前在线人数:"+ allout.size());//给服务器端看的
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** 广播消息给所有客户端*/
private void sendMessage(String message){
synchronized (Server.class) {//避免处理数组时影响遍历
for(PrintWriter p:allout){
p.println(message);
}
}
到此服务端的流程就算处理完毕了,当然不要忘记写main方法,在main方法里边实例化Server并调用server.start()即可
1.3 客户端Client:
一个客户端需要什么?它仅仅需要一个私有的socket变量即可
首先我们来写构造方法:
public Client(){
try {
//实例化Socket时需要传入两个参数
//下面两个参数就是远端计算机的地址和远端计算机开放的端口
System.out.println("正在连接服务端...");
socket =new Socket("localhost",8088);//127.0.0.1,这里的“localhost”字段是可以指定为你能够成功连接的电脑的ip的
System.out.println("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
然后想,客户端要做的事情有两件,发送信息和接收信息
和服务端不同,服务端每接收到一条信息就直接转发,接收和转发是连在一起的,二者可以说是一个整体,一个线程足以搞定,服务端本身的线程只需要等待客户端连接即可。而客户端则不同,收发信息两者需要互不干扰,但客户端本身只需要考虑自己,不需要等待别的客户端接入的事务,因此不需要将两件事都建立线程,在这里我们将接收信息作为一条线程分离出去
这样,我们的start方法里边一部分代码用来分配线程,一部分用来对发送事件直接处理
public void start(){
try {
//启动一个线程来读取服务端发送的消息
ServerHandler handler=new ServerHandler();
Thread t=new Thread(handler);
//将读取服务器消息的线程设置为守护线程
//这样当我们停止给服务端发信息后(即主线程结束,进程没有其他用户线程活着)
//守护线程就将被杀死
//t.setDaemon(true);
t.start();
/**
* 通过Socket的方法:
* OutputStream getOutputStream()
* 获取的字节输出流会通过网络发送给远端建立好连接的客户端
*/
OutputStream out=socket.getOutputStream();//字节输出流
OutputStreamWriter osw=new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw=new BufferedWriter(osw);
PrintWriter pw=new PrintWriter(bw,true);//发的时候才需要flush,防止发不过去
pw.println("你好服务端!");
Scanner scanner=new Scanner(System.in);
String line=null;
while (true){
line =scanner.nextLine();
pw.println(line);
if(line.equalsIgnoreCase("exit")){
System.out.println("##客户端退出");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();//与远端计算机断开连接,进行tcp挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
接收信息的线程
private class ServerHandler implements Runnable{
@Override
public void run() {//线程的run方法不允许throws,所以里边的异常只能try-catch
//读取服务器回的消息
try {
InputStream in=socket.getInputStream();//字节流
InputStreamReader isr=new InputStreamReader(in);//转换为字符流
BufferedReader br=new BufferedReader(isr);//加缓冲器
String line;
while((line=br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
//这里不输出异常错误信息了,当远端计算机断开时会出现异常,可以不输出错误信息
}
}
}
到这里Client的流程也处理完毕,同样main方法中服务的起调记得加
总结
本文对Socket编程的聊天室进行了分析,并未涉及协议相关内容,属于Socket入门级程序,难点有java IO流转换,日后会对其进行解析,另外本聊天室并未实现私聊功能,下期上新