基于Socket通信的简单网络聊天室(1)

     通过这个简单的网络聊天室,来展示和说明Socket通信基本原理和执行方式。有什么不正不足之处,或者更好的见解,请各位牛人明示。文章的思路并非本人原创,仅仅为了自身对Socket通信编程的巩固,也借此分享给需要的人。

     TCP/IP协议是一种可靠的网络协议,是在议通信的两端各建立一个Socket,在其之间形成一个网路虚拟链路,建立好之后

两端的程序就可以通过虚拟链路进行通信。

    Java中使用Socket对象来代码两端的通信端口,且通过Socket产生的IO流进行网络通信。

    TCP通信是没有服务端、客户端区分的。在没有正式建立起虚拟链路前,必须有一方主动接收其他通信实体的链接请求。在Java中,ServerSocket负责承担此类任务。它负责监听并接受来自客户端Socket的通信请求,如果没有通信请求,则一直处于等待状态。ServerSocket包含一个方法 accept() ,当接收到一个通信请求之后,此方法则会返回一个与客户端对应Socket对象,如果没有,则处于等待状态,线程也会处于阻塞状态直到有来自客户端的通信请求

   

public class Server 
{
	private static final int SERVER_PORT = 3003;
	//使用MyMap对象来保存每个客户名字和对应输出流之间的对应关系。
	public static YeekuMap<String , PrintStream> clients =
		new YeekuMap<String , PrintStream>();
	public void init()
	{
		ServerSocket ss = null;
		try
		{
			//建立监听的ServerSocket
			ss = new ServerSocket(SERVER_PORT);
			//采用死循环来不断接受来自客户端的请求
			int i = 0;
			while(true)
			{
				Socket socket = ss.accept();
				new ServerThread_1(socket).start();
				System.out.println("---》接受一个客户端请求,创建一个线程:\t" + i);
				i++;
			}
		}
		//如果抛出异常
		catch (IOException ex)
		{
			System.out.println("服务器启动失败,是否端口" 
				+ SERVER_PORT + "已被占用?");
		}
		//使用finally块来关闭资源
		finally
		{
			try
			{
				if (ss != null)
				{
					ss.close();
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
			System.exit(1);
		}
	}
    public static void main(String[] args)
    {
		Server server = new Server();
		server.init();
    }
}

     用一个指定端口号来创建一个ServerSocket,端口号的有效数值应该是在0~65535

ServerSocket ss = new ServerSocket(3000);

   建立一个类似与服务端的Sever类,在Server类的初始化模块中创建通信监听器,并在循环体中不断调用accept()方法接收来自其他客户端的通信请求,当接收到来自客户端的通信请求时accept()方法会返回一个与客户端对应的Socket通信实体,将其传入到一个通信线程中处理来自客户端的通信信息。如果没有接受到来自客户端的通信请求,其所在的线程将一直处于阻塞状态。

   下面是服务器通信线程的管理类,主要负责处理来自客户端通信信息的处理

 

 

package com.base.Net.Senior;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;


/**
 * 服务器线程管理类
 * @author Petter
 *
 */
public class ServerThread_1 extends Thread{
	private Socket socket;
	BufferedReader buffReader;
	PrintStream printStream;
	/**
	 * 定义构造函数,用于接受一个Socket来创建ServerThread线程
	 * @param socket
	 */
	public ServerThread_1(Socket socket){
		this.socket = socket;
	}
	@Override
	public void run() {
		try{
			//获取该Socket对应的输入流
			buffReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			//获取该Socket对应的输出流
			printStream = new PrintStream(socket.getOutputStream());
			String line = null;
			while( (line = buffReader.readLine()) != null ){
				//如果读到的行以MyProtocol.USER_ROUND开始,并以其结束
				//可以确定读到的是用户登录的用户名
				if(line.startsWith(YeekuProtocol.USER_ROUND) && line.endsWith(YeekuProtocol.USER_ROUND)){
					//获取真实的数据
					String userName = getRealMsg(line);
					System.out.println("---》用户: " + userName);
					//如果用户名重复
					if(Server.clients.containsKey(userName)){
						System.out.println("登录的用户名重复!");
						printStream.println(YeekuProtocol.NAME_REP);
					}else{
						System.out.println("登录到服务器成功!");
						printStream.println(YeekuProtocol.LOGIN_SUCCESS);
						Server.clients.put(userName, printStream);
					}
				}
				//如果得到的行以YeeKuProtocol.PRIVATE_ROUND开始,并以起结束
				//可以确定是私聊信息,私聊信息只向特定的输出流发送
				else if(line.startsWith(YeekuProtocol.PRIVATE_ROUND) && line.endsWith(YeekuProtocol.PRIVATE_ROUND)){
					//得到真实消息
					String userAndMsg = getRealMsg(line);
					//以SPLIT_SIGN来分割字符串,前面部分是私聊用户,后面部分是聊天信息
					String user = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[0];
					String msg =  userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[1];
					//获取私聊用户对于的输出流
					Server.clients.get(user).println(
							Server.clients.getKeyByValue(printStream) + "悄悄的对你说:" + msg);
				}
				//公聊信息,向每个Socket发送
				else{
					//得到真实消息
					String msg = getRealMsg(line);
					//遍历Clients中的每个输出流
					for(PrintStream clientsPs : Server.clients.valueSet()){
						clientsPs.println(Server.clients.getKeyByValue(printStream) + "说:" +  msg);
					}
				}
			}
			//捕获异常后, 表明该Socket对应的客户端已经出现了问题
			//所以程序将对应的输出流从Map中删除
		}catch(IOException e){
			Server.clients.removeByValue(printStream);
			System.out.println(Server.clients.size());
		}finally{
			if(buffReader != null){
				try {
					buffReader.close();
					if(printStream != null){
						printStream.close();
					}
					if(socket != null){
						socket.close();
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			
		}
	}
	/**
	 * 将读取到的内容去掉前后协议字符串,恢复真实的数据
	 * @param line
	 * @return
	 */
	public String getRealMsg(String line){
		return line.substring(YeekuProtocol.protocol_len, line.length() - YeekuProtocol.protocol_len);
	}
}

   在通信线程管理类中,会用到 YeekuProtocol这样的通信协议类,主要是为了在接受到通信信息时区分信息种类,是公聊信息还是私聊

信息还是用户登录。

   同时,为了将某个用户的公共聊天数据或者私密聊天数据发送给其他用户,在客户端做出请求时,会再通信线程管理类中将其对应的输出流PrintStream类实体,userName保存到一个键值对的Map中,这样就可以在服务端区分是哪个用户发送的通信信息,是以公共聊天的形式发送给其他所有在线用户还是 以私密形式发送给指定的在线用户

  

package com.base.Net.Senior;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

//扩展HashMap类,MyMap类要求value也不可重复
public class YeekuMap<K,V> extends HashMap<K,V>
{
	/**
	 * 
	 */
	private static final long serialVersionUID = -1731258447378994845L;
	//根据value来删除指定项
	public void removeByValue(Object value) 
	{
		for (Object key : keySet())
		{
			if (get(key) == value)
			{
				remove(key);
				break;
			}
		}
	}

	//获取所有value组成的Set集合
	public Set<V> valueSet() 
	{
		Set<V> result = new HashSet<V>();
		//遍历所有key组成的集合
		for (K key : keySet())
		{
			//将每个key对应的value添加到result集合中
			result.add(get(key));
		}
		return result;
	}

	//根据value查找key。
	public K getKeyByValue(V val) 
	{
		//遍历所有key组成的集合
		for (K key : keySet())
		{
			//如果指定key对应的value与被搜索的value相同
			//则返回对应的key
			if (get(key).equals(val) 
				&& get(key) == val)
			{
				return key;
			}
		}
		return null;
	}
	//重写HashMap的put方法,该方法不允许value重复
	public V put(K key,V value)
	{
		//遍历所有value组成的集合
		for (V val : valueSet() )
		{
			//如果指定value与试图放入集合的value相同
			//则抛出一个RuntimeException异常
			if (val.equals(value) 
				&& val.hashCode() == value.hashCode())
			{
				throw new RuntimeException
					("MyMap实例中不允许有重复value!"); 
			}
		}
		return super.put(key , value);
	}
}

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
里面包含聊天室的客户端和服务器端的源文件和一份完整的设计报告。 一、 系统概要 本系统能实现基于VC++的网络聊天室系统。有单独的客户端、服务器端。 服务器应用程序能够接受来自客户端的广播,然后向客户端发送本机的IP与服务端口,让客户端接入到服务器进行聊天,检测用户名是否合法(重复),服务器责接收来自客户端的聊天信息,并根据用户的需求发送给指定的人或所有人,能够给出上线下线提示。客户端能够发出连接请求,能编辑发送信息,可以指定发给单人或所有人,能显示聊天人数,上线下线用户等。 二、 通信规范的制定 服务请求规范: 服务器端: (1) 创建一个UDP的套接字,接受来自客户端的广播请求,当请求报文内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”时,接受请求,给客户端发送本服务器TCP聊天室的端口号。 (2) 创建一个主要的TCP协议的套接字负责客户端TCP连接 ,处理它的连接请求事件。 (3)在主要的TCP连接协议的套接字里面再创建TCP套接字保存到动态数组里,在主要的套接字接受请求后 ,就用这些套接字和客户端发送和接受数据。 客户端: (1) 当用户按“连接”按钮时,创建UDP协议套接字,给本地计算机发广播,广播内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”。 (2)当收到服务器端的回应,收到服务器发来的端口号后,关闭UDP连接。根据服务器的IP地址和端口号重新创建TCP连接。 故我思考:客户端一定要知道服务器的一个端口,我假设它知道服务器UDP服务的端口,通过发广播给服务器的UDP服务套接字,然后等待该套接字发回服务器TCP聊天室服务的端口号,IP地址用ReceiveForom也苛刻得到。 通信规范 通信规范的制定主要跟老师给出的差不多,并做了一小点增加: (增加验证用户名是否与聊天室已有用户重复,在服务器给客户端的消息中,增加标志0) ① TCP/IP数据通信 --- “聊天”消息传输格式 客户机 - 服务器 (1)传输“用户名” STX+1+用户名+ETX (2) 悄悄话 STX+2+用户名+”,”+内容+ETX (3) 对所有人说 STX+3+内容+ETX 服务器- 客户机 (0)请求用户名与在线用户名重复 //改进 STX+0+用户名+EXT (1)首次传输在线用户名 STX+1+用户名+ETX (2)传输新到用户名 STX+2+用户名+ETX (3)传输离线用户名 STX+3+用户名+ETX (4)传输聊天数据 STX+4+内容+ETX (注:STX为CHR(2),ETX 为CHR(3)) 三、 主要模块的设计分析 四、 系统运行效果 (要求有屏幕截图) 五、 心得与体会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值