碎碎念

先谈谈我们要实现的效果:客户端可以选择要聊天的对象,或者直接广播消息(类似QQ的私聊和群消息),支持图片发送(简单的)

那么,该如何实现呢?

首先明确的是,要分客户端和服务器端两个部分(废话)

客户端:选择要发送的对象,发送信息。同时有一个线程在监听是否收到新的信息。

服务器端:负责转发收到的消息,并负责管理所有接入的连接

好了有了大体思路后,开始编程吧~

客户端

界面设计

客户端要提供的信息主要是发送对象、发送信息内容,故设计如下:

其中用户名必须提供(这里考虑的比较简单,不需要验证用户名是否重复),发送信息时需要选择目标用户。

编码实现

连接服务器部分

连接服务器和正常的tcp连接没什么区别,由于要考虑到 目标用户 选项刷新的问题,这里必须在建立连接后向服务器发送一条信息告知服务器自己的身份,服务器接收后会再返回一条信息来告知客户端目前服务器在线用户的名称。

因为请求的信息内容、作用不一样,这里使用自定义的“信息格式”,使用$符号来分割,请求格式为 code$message

以下是请求的说明表

故我们可以根据该表写出一个Encode函数:

 private String EncodeMessage(String message, int code,String goalName)
{
switch (code)
{
case ://汇报用户名
return "1$" + message;
case ://发送信息
return "2$" + message+"$"+goalName;
case ://断开连接
return "3$" + message;
default:
return "-1$错误";
}
}

紧接着对其进行发送信息功能进行封装:

  public void SendMessage(String message, int code, String goalName)
{
String sendmessage = EncodeMessage(message, code, goalName);
try
{
bw.Write(sendmessage);
bw.Flush();
log = DateUtil.getTime() + "发送信息:" + message;//日志
if (code != )//1是第一次建立连接的时候发送的自己用户名,所以没必要打印出来,故这里加了一个判断
{
textbox_chatbox.AppendText(log); }
else
{
flag_open = true;//该标志是用来控制接收信息的循环的,下面再讲
}
}
catch//捕获异常是为了防止服务器意外断开连接
{
log = DateUtil.getTime() + "服务器已断开连接";
return;
}
}

好了下面开始主体tcp连接代码:

        //全局变量声明
private const int port = ;
private TcpClient tcpClient;
private NetworkStream networkStream;
private BinaryReader br;
private BinaryWriter bw;
private String log = "";
private Boolean flag_open = false; //初始化
private void button_connect_Click(object sender, EventArgs e)
{
//开始连接服务器,同步方式阻塞进行 IPHostEntry remoteHost = Dns.GetHostEntry(textbox_ip.Text);
tcpClient = new TcpClient();
tcpClient.Connect(remoteHost.HostName, port);//阻塞啦!!!
if (tcpClient != null)
{
String username = textBox_name.Text;
log = DateUtil.getTime() + "以用户名为 "+username+"连接服务器";
textbox_chatbox.AppendText(log);
networkStream = tcpClient.GetStream();
br = new BinaryReader(networkStream);
bw = new BinaryWriter(networkStream);
SendMessage(username, ,"");//向服务器发送信息,告诉服务器自己的用户名 Thread thread = new Thread(ReceiveMessage);//开一个新的线程来接收信息
thread.Start();
thread.IsBackground = true;//线程自动关闭
}
else
{
log = DateUtil.getTime() + "连接服务器失败,请重试";
textbox_chatbox.AppendText(log);
}
}

接收信息部分

为了程序的人性化,接收信息一定是自动接收,这里使用线程来实现。因为接收信息也是阻塞,故新开一个线程并使用while循环一直监听,有消息进来就更新。

因此我们也需要规定服务器发过来的信息的格式,如下图所示:

因此同样我们可以写出解析函数:

  private void DecodeMessage(String message)
{
String[] results = message.Split('$');
int code = int.Parse(results[]);
switch (code)
{
case ://更新的是用户
comboBox1.Invoke(updateComboBox, message);//委托,更新下拉框内容
break;
case ://收到信息
String rev = message.Substring(message.IndexOf('$')+);
textbox_chatbox.Invoke(showLog,DateUtil.getTime()+rev);//打印在日志
break;
} }

接收信息函数:

 public void ReceiveMessage()
{
while (flag_open)
{
try
{
string rcvMsgStr = br.ReadString();
DecodeMessage(rcvMsgStr);
}
catch
{
log = DateUtil.getTime() + "服务器已断开连接";
textbox_chatbox.Invoke(showLog,log);
return;
}
}
}

对应的委托函数自己根据你的命名写就可以啦~这里就不再赘述

终止连接

终止连接的思路也很简单:向服务器发送消息通知服务器我要下线了,然后关闭相应的流即可。

private void button_stop_Click(object sender, EventArgs e)
{
SendMessage(textBox_name.Text, ,"");
log = DateUtil.getTime() + "已发起下线请求";
textbox_chatbox.Invoke(showLog, log);
flag_open = false;
if (bw != null)
{
bw.Close();
}
if (br != null)
{
br.Close();
}
if (tcpClient != null)
{
tcpClient.Close();
}
}

至此客户端基本完成,细节你们可以再优化优化~

服务器端

服务器端是挺复杂的,我的思路是

线程1:循环监听是否有新的客户端连接加入,若有则加入容器中,并向容器中所有的连接广播一下目前在线的客户。

线程n:每一个连接都应该有一个线程循环监听是否有新的消息到来,有则回调给主线程去处理(这样不是很高效但基本满足需求)

界面设计

因为服务器只负责启动、暂停和转发消息,界面只需要日志窗口、状态口和两个按钮即可。(不是我懒)

编码实现

启动服务器部分

启动服务器,就需要开启一个新的线程来循环监听,来一个连接就要存入容器中去管理。

因为写习惯Java了,所以这里容器也选择List<>,首先我们先创建一个Client类来封装一些方法。

在编写客户端的时候我们知道,每一个客户端都应该有相应的名称,所以Client类一定要包括一个名称以及相应的连接类。

 public String userName;
public TcpClient tcpClient;
public BinaryReader br;
public BinaryWriter bw;

发送信息函数类似客户端,直接调用bw即可。但接收信息必须是一个线程循环监听,故需要设计一个接口来实现新消息来临就回调传给主线程操作。

  public interface ReceiveMessageListener
{
void getMessage(String accountName,String message);
}

顺便把名字传过来可以知道到底是谁发送的消息。

Client类的总体代码如下:

 class Client
{
public String userName;
public TcpClient tcpClient;
public BinaryReader br;
public BinaryWriter bw;
public ReceiveMessageListener listener;
public bool flag = false; public Client(String userName,TcpClient client,ReceiveMessageListener receiveMessageListener)
{
this.userName = userName;
this.tcpClient = client;
this.listener = receiveMessageListener;
NetworkStream networkStream = tcpClient.GetStream();
br = new BinaryReader(networkStream);
bw = new BinaryWriter(networkStream);
Thread thread = new Thread(receiveMessage);
thread.Start();
flag = true;
thread.IsBackground = true;
} public override bool Equals(object obj)
{
return obj is Client client &&
userName == client.userName;
}
public bool sendMessage(String ecodeMessage)
{
try
{
bw.Write(ecodeMessage);
bw.Flush();
return true;
}catch {
return false;
} } public void receiveMessage()
{
while (true)
{
try
{
String temp = br.ReadString();
listener.getMessage(userName, temp);
}
catch
{
return;
}
} }
public void stop()
{
flag = false;
if (bw != null)
{
bw.Close();
}
if (br != null)
{
br.Close();
}
if (tcpClient != null)
{
tcpClient.Close();
}
} public interface ReceiveMessageListener
{
void getMessage(String accountName,String message);
}
}

写好Client以后我们就可以准备编写启动服务器的代码了,步骤:启动服务器->监听->新客户来->加入List->更新(广播)用户表->继续监听

 private void StartServer()
{
log = getTime() + "开始启动服务器中。。。";
textBox_log.Invoke(showLog, log);
tcpListener = new TcpListener(localAddress, port);
tcpListener.Start();
log = getTime() + "IP:" + localAddress + " 端口号:" + port + " 已启用监听";
textBox_log.Invoke(showLog, log);
while (true)
{
try
{
tcpClient = tcpListener.AcceptTcpClient();
networkStream = tcpClient.GetStream();
br = new BinaryReader(networkStream);
bw = new BinaryWriter(networkStream);
String accountName =br.ReadString();
accountName = decodeUserName(accountName);
log = getTime() + "用户:"+accountName+"已上线";
count++;
label_status.Invoke(showNumber);
textBox_log.Invoke(showLog, log);
clientList.Add(new Client(accountName,tcpClient,listener));
notifyUpdateUserList();
}
catch
{
log = getTime() + "已终止监听";
textBox_log.Invoke(showLog, log);
return;
}
} }

启动服务器只需要开启新线程就行了~

 Thread thread = new Thread(StartServer);
thread.Start();
thread.IsBackground = true;

更新名称函数:

 private void notifyUpdateUserList()
{
String message = "" + getCurUserName();
foreach (Client i in clientList)
{
i.sendMessage(message);
}
}
  private String getCurUserName()
{
String aa = "";
foreach(Client i in clientList)
{
aa = aa + "$" + i.userName;
}
return aa;
}

回调接口实现、接收信息处理

在创建Client的时候需要传入一个监听接口,我们自己创建一个类来实现:

根据之前设置的信息传送格式,写出对应的处理函数

public class MyListener : Client.ReceiveMessageListener
{
public Form1 f;
public MyListener(Form1 form)
{
f = form;
}
public void getMessage(String accountname,string message)
{
//TODO
string []results = message.Split('$');
if (int.Parse(results[]) == )//发送信息
{
String content = results[];
String goalName = results[];
f.SendMessageToClient(content,goalName,accountname);
}else if (int.Parse(results[]) ==)//终止连接
{
String content = results[];
f.stopClientByName(content);
}
else
{
//请求add
}
}
}

转发信息的逻辑:拿到目标用户名称,判断是不是所有人(广播)若是则广播,若不是则再去遍历寻找对应的客户再发送。

private void SendMessageToClient(String content,String goalName,String userName)
{
bool flag = false;
if (goalName.Equals("所有人"))
{
flag = true;
}
foreach(Client i in clientList)
{
if (flag)
{
i.sendMessage("2$广播:" + userName+"说: "+content);
}
else
{
if (i.userName.Equals(goalName))
{
i.sendMessage("2$" + userName + "说: "+content);
return;
}
} } }

关闭对应客户端连接的思路:遍历

 public void stopClientByName(String name)
{
foreach(Client i in clientList){
if (i.userName.Equals(name))
{
i.stop();
count--;
label_status.Invoke(showNumber);
textBox_log.Invoke(showLog, getTime() + name + "已下线");
clientList.Remove(i);
}
}
}

停止服务器部分

先断开所有在线客户端的连接,再断开总的。

 private void button_stop_Click(object sender, EventArgs e)
{
CloseAllClients();
if (bw != null)
{
bw.Close();
}
if (br != null)
{
br.Close();
}
if (tcpClient != null)
{
tcpClient.Close();
}
if (tcpListener != null)
{
tcpListener.Stop();
}
log = getTime() + "已停止服务器";
textBox_log.Invoke(showLog, log);
}
  public void CloseAllClients()
{
foreach(Client i in clientList)
{
i.stop();
}
clientList.Clear();
}

完成。

补充:图片接收发送(二进制数据)

在网上搜寻资料的时候看到有大佬对图片进行Base64编码然后生成字符串来收发,不知道可行与否。如果可行那么直接在原来的规则增加一条图片规则即可。具体方法看这个:点我跳转

我选择的是直接发送byte数组

设计一下收发规则,在原来的基础上增加:

发送格式:

服务器返回格式:

思路

发送string类型信息给服务器,通知服务器我要发送图片了,并且直接把图片的byte大小传过去

紧接着直接把这个byte数组发过去

服务器接收string信息后根据拿到得大小去读这个byte数组,转发即可。

实现

图片编码

//把byte转图片,支持gif
public Image SetByteToImage(byte[] mybyte)
{
MemoryStream ms = new MemoryStream(mybyte);
Image outputImg = Image.FromStream(ms);
return outputImg;
}
//把图片转byte[] 设置读取文件为允许修改
private byte[] SetImageToByteArray(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite);
byte[] byteData = new byte[fs.Length];
fs.Read(byteData, , byteData.Length);
fs.Close();
return byteData;
}

选择图片

 OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.Filter = "图片文件(*.jpg,*.gif,*.bmp,*.png)|*.jpg;*.gif;*.bmp;*.png";
DialogResult result = fileDialog.ShowDialog();
if (result == DialogResult.OK)
{
Pic_dir = fileDialog.FileName;//Pic_dir就是一个string来存放图片地址
pic_show.Image = Image.FromFile(Pic_dir); }

发送图片

byte[] datas =SetImageToByteArray(Pic_dir);
bw.Write(datas, , datas.Length);
bw.Flush();

接收图片

br.ReadBytes(传过来的长度)

服务器端

服务器端改动不大,主要是Client里面要加一个直接读byte的方法

或者修改接口把br返回回来:

public interface ReceiveMessageListener
{
void getMessage(String accountName,String message, BinaryReader br, BinaryWriter bw);
}

效果图

总结

因为代码是我在很短时间内敲出来的,如果有不妥或者不足之处欢迎指正。

当你掌握了一对一(一个客户端和一个服务器端连接)这种形式以后再去看多人聊天,也是很简单的,关键是多线程的使用以及回调。接口返回数据这种形式真的太重要了,在这里用的也非常方便。

同时消息传送格式也很关键,尤其是当你在服务器端加入一些功能后,通信之间传输的是指令还是消息,都必须很好地区别出来。

我在文中的写法不是特别建议,最好是单独抽出来写成一个类,这样以后维护方便、看起来简洁明了,不像我这个都杂在一起了。。。

写本文章主要是总结一下自己编码实现的思路,关键代码都已经放在上面了,相信你按照我的步骤和思路来应该都能做出来,不自己做只是复制粘贴是没用的(而且也没啥专业代码嗯,自己写写呗),当然大佬请绕路。

源代码:ChatBoxDemo

下面放一张运行截图(人格分裂):

【C#】写一个支持多人聊天的TCP程序的更多相关文章

  1. 五:用JAVA写一个阿里云VPC Open API调用程序

    用JAVA写一个阿里云VPC Open API调用程序 摘要:用JAVA拼出来Open API的URL 引言 VPC提供了丰富的API接口,让网络工程是可以通过API调用的方式管理网络资源.用程序和软 ...

  2. #写一个随机产生138开头手机号的程序 1.输入一个数量,产生xx条手机号 2.产生的这些手机号不能重复

    import randomcount=int(input('请输入你所想要手机号数量:'))prefix='138'for i in range(count): num=random.sample(r ...

  3. python 写一个生成大乐透号码的程序

    """ 写一个生成大乐透号码的程序 生成随机号码:大乐透分前区号码和后区号码, 前区号码是从01-35中无重复地取5个号码, 后区号码是从01-12中无重复地取2个号码, ...

  4. 第二十三个知识点:写一个实现蒙哥马利算法的C程序

    第二十三个知识点:写一个实现蒙哥马利算法的C程序 这次博客我将通过对蒙哥马利算法的一个实际的实现,来补充我们上周蒙哥马利算法的理论方面.这个用C语言实现的蒙哥马利算法,是为一个位数为64的计算机编写的 ...

  5. android 开发 写一个RecyclerView布局的聊天室,并且添加RecyclerView的点击事件

    实现思维顺序: 1.首先我们需要准备2张.9的png图片(一张图片为左边聊天泡泡,一个图片为右边的聊天泡泡),可以使用draw9patch.bat工具制作,任何图片导入到drawable中. 2.需要 ...

  6. [NodeJS]使用Node.js写一个简单的在线聊天室

    声明:教程来自<Node即学即用>.源代码案例均出自此书.博文仅为个人学习笔记. 第一步:创建一个聊天server. 首先,我们先来写一个Server: var net = require ...

  7. 一道面试题:用shell写一个从1加到100的程序

    [试题描述] 请用shell写一个简短的程序,实现1+2+...+100的功能. [程序] 方法一: #!/bin/bash ..} do let sum+=$i done echo $sum 方法二 ...

  8. 网络编程—【自己动手】用C语言写一个基于服务器和客户端(TCP)!

    如果想要自己写一个服务器和客户端,我们需要掌握一定的网络编程技术,个人认为,网络编程中最关键的就是这个东西--socket(套接字). socket(套接字):简单来讲,socket就是用于描述IP地 ...

  9. java 用socket制作一个简易多人聊天室

    代码: 服务器端Server import java.io.*; import java.net.*; import java.util.ArrayList; public class Server{ ...

随机推荐

  1. (转)C++中的delete

    转载自:http://blog.csdn.net/sparkliang/article/details/3670930 为了防止内存泄漏,每一个动态内存分配必须有一个等同相反的内存释放操作,delet ...

  2. Docker Compose 文件讲解

    Docker Compose 是什么 官方文档: Docker Compose是定义和运行多容器 Docker 应用程序的工具.使用"Compose",您可以使用 YAML 文件来 ...

  3. 小白学 Python 数据分析(17):Matplotlib(二)基础操作

    人生苦短,我用 Python 前文传送门: 小白学 Python 数据分析(1):数据分析基础 小白学 Python 数据分析(2):Pandas (一)概述 小白学 Python 数据分析(3):P ...

  4. 【Weiss】简单说一下这一分类下的东西

    主要是学习资料<数据结构与算法分析>(Weiss)的习题 除去习题外,每一章主要用到的数据结构先会写一个版本放上来,包括数据结构代码与测试用代码 这种先行上传的代码只具有基本的功能,毕竟一 ...

  5. .NET的资源并不限于.resx文件

    为了构建一个轻量级的资源管理框架以满足简单的本地化(Localization)的需求,我试图直接对现有的Resource编程模型进行扩展.虽然最终没能满足我们的需求,但是这两天也算对.NET如何进行资 ...

  6. [Docker6] Docker compose多容器运行与管理

    六.Docker compose docker compose就是通过yml文件来定义和运行多个容器docker应用程序的工具,三步过程就能跑起一个compose: 定义应用程序的环境(yml中) 定 ...

  7. sqoop面试题

    1.1 Sqoop 在工作中的定位是会用就行1.1.1 Sqoop导入数据到hdfs中的参数 /opt/module/sqoop/bin/sqoop import \ --connect \ # 特殊 ...

  8. Linux常用命令 - cat命令详解

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 获取t ...

  9. 科普 | ​生成对抗网络(GAN)的发展史

    来源:https://en.wikipedia.org/wiki/Edmond_de_Belamy 五年前,Generative Adversarial Networks(GANs)在深度学习领域掀起 ...

  10. Taro 和 uni-app选型对比

    一.Taro和uni-app的介绍 1.taro的介绍 taro是多端统一开发框架,支持用 React 的开发方式编写一次代码,生成能运行在微信/百度/支付宝/字节跳动小程序.H5.React Nat ...