功能:
1.群聊
2.私聊:发送信息格式 @昵称:xxxx(xxxx为信息内容)
代码如下:
客户端
package chat_socket.copy;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
*/
public class Cilent {
private Socket socket;
//构造方法初始化客户端--两个参数:1:服务端计算机的IP地址 2.服务端应用程序的端口(int类型 0-65535)
public Cilent() throws Exception{
//创建Socket对象时就会尝试根据给定的地址(localhost)和端口(8088)连接服务器
socket = new Socket("localhost",8088);
}
//调用开始方法
public void start(){
//单独建立一个线程来读取服务端发送过来的信息
Thread t = new Thread(new ClientThread());
t.start();
Scanner scan = new Scanner(System.in);
//输入流-向服务端发送信息
try {
//通过socket获取输出流(抽象类)
OutputStream out = socket.getOutputStream();
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
out,"UTF-8"),true);
System.out.println("欢迎来到聊天室!");
//输入昵称
System.out.println("请输入昵称:");
String regex = "\\w+";
while(true){
String nickname = scan.nextLine();
if(nickname.matches(regex)){
pw.println(nickname);
break;
}
System.out.println("请从新输入:");
}
String message = null;
while(true){
message = scan.nextLine();
pw.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Cilent c = new Cilent();
c.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("客户端初始化失败!");
}
}
//建立一个线程来读取服务器发过来的信息,并输出到控制台
private class ClientThread implements Runnable{
public void run(){
try{
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(
in,"UTF-8"));
String message = null;
//循环读取服务器发送的每一个字符串
while((message = br.readLine())!= null){
System.out.println(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
服务端代码如下:
package chat_socket.copy;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务端
*/
public class Server {
private ServerSocket server;
//创建线程池,用于管理客户端的连接-在构造方法里初始化
private ExecutorService threadPool;
//输出流的一个集合-每创建一个连接,就将该连接的输出流加入集合
private Map<String,PrintWriter> map;
//构造方法,初始化服务端,指定服务端应用程序端口-抛出异常,让调用者处理
public Server() throws Exception{
server = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50);
//存放所有客户端输出流的集合--key是昵称 value是输出流
map = new HashMap<String,PrintWriter>();
}
//调用开始方法
public void start(){
try {
while(true){
//用于监听8088端口,若一个客户端连接,则返回一个Socket类型实例
System.out.println("等待客户端连接......");
Socket socket = server.accept();
//用连接池,没建立一个连接就放入线程池
ServerThread m = new ServerThread(socket);
threadPool.execute(m);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Server s = new Server();
s.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("服务端初始化失败!");
}
}
//锁的是方法所属的对象(Server类型的s对象)
//将给定的输出流存入共享集合----注意线程的安全性使用synchronized
public synchronized void addMap(String nickname,PrintWriter pw){
map.put(nickname, pw);
}
//将给定的输出流从共享集合中删除----注意线程的安全性使用synchronized
public synchronized void removeMap(String nickname){
map.remove(nickname);
}
//将线程收到的消息通过集合中的流依次发送给每个客户端---群发----注意线程的安全性使用synchronized
public synchronized void sendMessageAll(String message){//群聊
Collection<PrintWriter> c = map.values();
for(PrintWriter pw : c){
pw.println(message);
}
}
//根据制定的昵称输出--私聊
public synchronized void sendMessageSingle(String nickname,String message){
String name = message.substring(1, message.indexOf(":"));
String mess = message. substring(message.indexOf(":")+1);
map.get(name).println("["+nickname+"]说:"+mess);
}
//建立线程,每连接一个客户端,开启一个线程--内部类(在外部建立内部类对象,并调用外部类中属性和方法)
public class ServerThread implements Runnable{
private Socket socket;
private String ipname;//备用ip地址
private int port;//备用端口号
private String nickname;//昵称
public ServerThread(Socket socket){
this.socket = socket;
}
public void run(){
//因为流是用socket创建的,所以不用在finally中关闭流,即不用在外部声明null
//但是因为要在finally中要使用(remove去掉pw输出流)),所以要在外面进行声明
PrintWriter pw = null;
try{
//通过socket获取输入流、输出流(抽象类)
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//创建输入流读取信息
BufferedReader br = new BufferedReader(
new InputStreamReader(
in,"UTF-8"));
//创建好输入流后读取到的第一个字符串为昵称
nickname = br.readLine();
//创建输出流写信息
pw = new PrintWriter(
new OutputStreamWriter(
out,"UTF-8"),true);
//将输出流加入集合
addMap(nickname, pw);
//输出当前在线人数(服务端显示)
System.out.println("当前在线人数:"+map.size());
//通知所有在线用户xxx上线了
sendMessageAll("["+nickname+"]上线了");
/*
//获取客户端地址对象
InetAddress address = socket.getInetAddress();
获取客户端IP(完全限定名)
String ipname = address.getCanonicalHostName();
获取IP地址--一般使用这个
ipname = address.getHostAddress();
获取客户端的端口号
port = socket.getPort();
*/
/*
* 读取客户端发送过来的消息,当客户端断开连接
* 时,由于客户端系统不同,这里readLine方法的
* 执行结果也不同:
* 当windows的客户端断开后:这里会抛出异常
* 当linux的客户端断开后:这里会返回null
*/
String message = null;
//linux在此处会跳出循环
while((message = br.readLine())!= null){
if(message.startsWith("@")){
sendMessageSingle(nickname,message);
}else{
sendMessageAll("["+nickname+"]说:"+message);
}
}
} catch(Exception e){
//此处可以不抛异常,这样断开后就不会报异常
//e.printStackTrace();
}finally {
sendMessageAll("["+nickname+"]下线了");
//将该连接的输出流从共享集合中删除
removeMap(nickname);
//输出当前在线人数
System.out.println("当前在线人数为:"+map.size());
//断开后,不用关闭流,直接关闭socket
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
项目关键:
1.建立连接通过
2.能够从Client发送至Server,且Server接收后返回给Cilent
3.用线程池创建线程,实现一对一与服务器建立连接通讯(每创建一个连接,即创建一个线程去运行)
4.为每一个线程指定nickname。将每个线程中的PrintWriter放入map集合(key为nickname value为pw),当该线程结束时,在finally中remove去掉
5.群聊时遍历map,私聊时指定私聊输入文字格式: @nickname:xxxx,截取nickname在map中get输出流,截取xxxx并发送
注意事项:
1.客户端不用关流,断开连接后,客户端会抛出异常后,自动结束
2.服务端不用关闭流,直接在finally中关闭连接socket即可
3.传输文字时候,最好指定字符集,不要默认
4.遍历map,put,remove时候要规定为互斥