利用Socket编程(TCP协议)完成聊天室

author: RoWe98(RexRowe)

利用Socket编程(TCP协议)完成聊天室

摘 要

使用基于TCP协议的Socket网络编程实现聊天室,TCP协议基于hex请求(Request)-响应(Response)模式,使用IO流实现数据的传输

二、设计平台

JAVA Socket编程

三、设计原理

TCP模型

avatar

C/S 模型
下图是基于TCP协议的客户端/服务器程序的一般流程:

avatar

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的:应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

利用Java的Socket类

ServerSocket 类的构造方法:

ServerSocket() 创建未绑定的服务器套接字
ServerSocket(int port) 创建绑定到指定端口的服务器套接字 
ServerSocket(int port,int backlog) 创建服务器套接字并将其绑定到指定的本地端口    号,并指定了积压。 
ServerSocket(int port,int backlog,InetAddress bindAddr) 创建一个具有指定端口    的服务器,侦听backlog和本地IP地址绑定。 

Socket类的构造方法:

Socket() 创建一个未连接的套接字,并使用系统默认类型的SocketImpl。
Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定    端口号。
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) 创建套接    字并将其连接到指定的远程端口上指定的远程地址。
Socket(Proxy proxy) 创建一个未连接的套接字,指定应该使用的代理类型(如果有    的话),无论其他任何设置如何。
Socket(SocketImpl impl) 使用用户指定的SocketImpl创建一个未连接的Socket
Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号。
Socket(String host, int port, InetAddress localAddr, int localPort)创建套接字并将其    连接到指定远程端口上的指定远程主机。 

Socket通信的基本原理:

avatar

思路:客户端使用Socket类新建一个Client对象,服务端使用ServerSocket类新建一个Server对象,使用IO流完成数据的传输和交互,客户端的输出流为服务端的输入流;服务端的输出流为客户端的输入流。

Socket编程使用TCP协议的基本步骤:

服务器创建ServerSocket,在指定端口监听并处理请求
客户端创建socket,向服务器发送请求

avatar

四、程序主要流程图


服务器端(Server)

avatar

客户端(Client)

avatar

五、程序代码

服务端(ChatServer.java)

1.新建一个ChatServer类完成服务端的编写

2.指定端口使用SocketServer创建服务器,阻塞式等待连接accept

public static void main(String[] args) throws IOException {
        System.out.println("-----Server-----");
        // 1、指定端口 使用ServerSocket创建服务器
        ServerSocket server =new ServerSocket(8889);
        // 2、阻塞式等待连接 accept
        while(true) {
                Socket  client =server.accept(); 
                System.out.println("一个客户端建立了连接");
                Channel c = new Channel(client);
                all.add(c);//使用容器管理所有成员客户端
                new Thread(c).start();            
            }        
        }
}

3.使用CopyOnWriteArrayList创建容器all从而管理所有的客户端成员

Private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<Channel>();

4.新建Channel类一个客户代表一个Channel并新建receive()方法接受消息,send()方法发送消息,sendOthers()方法完成私聊和群聊,release()方法释放资源。

/一个客户代表一个Channel
    static class Channel implements Runnable{
            private DataInputStream dis;
            private DataOutputStream dos;
            private Socket  client;            
            private boolean isRunning;
            private String name;
            public Channel(Socket  client) {
                this.client = client;
                try {
                    dis = new DataInputStream(client.getInputStream());
                    dos =new DataOutputStream(client.getOutputStream());
                    isRunning =true;
                    //获取名称
                    this.name = receive();
                    //欢迎你的到来
                    this.send("欢迎你的到来");
                    sendOthers(this.name+"来到了本聊天室",true);
                } catch (IOException e) {
                    System.out.println("---1------");
                    release();                    
                }            
            }

            //接收消息
            private String receive() {
                String msg ="";
                try {
                    msg =dis.readUTF();
                } catch (IOException e) {
                    System.out.println("---2------");
                    release();
                }
                return msg;
            }

            //发送消息
            private void send(String msg) {

                //时间显示
                Date date = new Date();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
                String currentTime = sdf.format(date);

                try {
                    dos.writeUTF(currentTime+"\r\n"+msg);
                    dos.flush();
                } catch (IOException e) {
                    //System.out.println("---3------");
                    release();
                }
            }



            //群聊
            //私聊约定数据格式:@xxx:msg
            private void sendOthers(String msg,boolean isSys) {
                boolean isPrivate = msg.startsWith("@");
                if(isPrivate) { //私聊
                    int idx = msg.indexOf(":");
                    //获取目标和数据
                    String targetName = msg.substring(1,idx);
                    msg = msg.substring(idx+1);
                    for(Channel other:all) {
                        if(other.name.equals(targetName)) {
                            other.send(this.name+"私聊你: "+msg);
                            break;
                        }
                    }
                }
                else {
                    for(Channel other:all) {
                        if(other==this) {//自己
                            continue;
                        }
                        if(!isSys) {
                            other.send(this.name+":"+msg);//群聊消息
                        }
                        else {
                            other.send(msg); //系统消息
                        }
                    }
                }    
            }
            //释放资源
            private void release() {
                this.isRunning = false;
                ChatUtils.close(dis,dos,client);
                //退出
                all.remove(this);
                sendOthers(this.name+"离开了本聊天室", false);
            }
            @Override
            public void run() {
                while(isRunning) {
                    String msg = receive();
                    if(!msg.equals("")) {
                        sendOthers(msg,false);
                    }
                }
            }

客户端(Client.java)

1.新建Client类完成客户端的编写

2.输入用户名新建连接,使用Socket创建客户端和指定的服务器端口和地址

3.客户端发送消息

public class Client {
    public static void main(String[] args) throws UnknownHostException,                 IOException {
        System.out.println("-----Client-----");
        BufferedReader br = new BufferedReader(new     InputStreamReader(System.in));
        System.out.println("请输入用户名:");
        String name = br.readLine();
        //1.建立连接,使用Socket创建客户端+服务的地址和端口
        Socket client = new Socket("localhost",8889);
        //2.客户端发送消息
        new Thread(new Send(client,name)).start();
        new Thread(new Receive(client)).start();
    }
}

Receive类(Receive.java)

1.使用多线程封装接收端

方法有接收消息,释放资源,重写run方法

public class Receive implements Runnable {
    private DataInputStream dis ;
    private Socket client;
    private boolean isRunning;
    public Receive(Socket client) {
        this.client = client;
        this.isRunning = true;
        try {
            dis =new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            System.out.println("====2=====");
            release();
        }
    }

    //接收消息
    private String receive() {
        String msg ="";
        try {
            msg =dis.readUTF();
        } catch (IOException e) {
            System.out.println("====4====");
            release();
        }
        return msg;
    }

    @Override
    public void run() {        
        while(isRunning) {
            String msg =receive();
            if(!msg.equals("")) {
                System.out.println(msg);
            }
        }
    }
    //释放资源
    private void release() {
        this.isRunning = false;
        ChatUtils.close(dis,client);
    }
}

Send类(Send.java)

1.使用多线程编写发送端

2.方法有从控制台获取信息,释放资源,重写多线程的run方法

public class Send implements Runnable{
    private BufferedReader console;
    private DataOutputStream dos;
    private Socket client;
    private boolean isRunning;
    private String name;
    public Send(Socket client,String name) {
        this.client = client;
        console = new BufferedReader(new InputStreamReader(System.in));
        this.isRunning = true;
        this.name = name;
        try {
            dos = new DataOutputStream(client.getOutputStream());
            //发送名称
            send(name);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            this.release();
        }
    }

    //从控制台获取消息
    private String getStrFromConsole() {
        try {
            return console.readLine();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "";
    }

    //发送消息
    private void send(String msg) {
        try {
            dos.writeUTF(msg);
            dos.flush();
        } catch (IOException e) {
            System.out.println("---client------");
            release();
        }
    }

    //释放资源
    private void release() {
        this.isRunning = false;
        ChatUtils.close(dos,client);
    }
    @Override
    public void run() {
        while(isRunning) {
            String msg = getStrFromConsole();
            if(!msg.equals("")) {
                send(msg);
            }
        }
    }

}

工具类(ChatUtils.java)

1.释放资源的工具类


public class ChatUtils {
    //释放资源
    public static void close(Closeable ...targets) {
        for(Closeable target:targets) {
            try {
                if(null != target) {
                    target.close();
                }
            }catch (Exception e) {
                // TODO: handle exception
                e.printStackTrace();
            }
        }
    }
}

运行效果

avatar

END