C# 网络编程之简易聊天示例

还记得刚刚开始接触编程开发时,傻傻的将网站开发和网络编程混为一谈,常常因分不清楚而引为笑柄。后来勉强分清楚,又因为各种各样的协议端口之类的名词而倍感神秘,所以为了揭开网络编程的神秘面纱,本文尝试以一个简单的小例子,简述在网络编程开发中涉及到的相关知识点,仅供学习分享使用,如有不足之处,还请指正。.

概述

在TCP/IP协议族中,传输层主要包括TCP和UDP两种通信协议,它们以不同的方式实现两台主机中的不同应用程序之间的数据传输,即数据的端到端传输。由于它们的实现方式不同,因此各有一套属于自己的端口号,且相互独立。采用五元组(协议,信源机IP地址,信源应用进程端口,信宿机IP地址,信宿应用进程端口)来描述两个应用进程之间的通信关联,这也是进行网络程序设计最基本的概念。传输控制协议(Transmission Control Protocol,TCP)提供一种面向连接的、可靠的数据传输服务,保证了端到端数据传输的可靠性。

C# 网络编程之简易聊天示例

涉及知识点

本例中涉及知识点如下所示:

  1. TcpClient :TcpClient类为TCP网络服务提供客户端连接,它构建于Socket类之上,以提供较高级别的TCP服务,提供了通过网络连接、发送和接收数据的简单方法。

  2. TcpListener:构建于Socket之上,提供了更高抽象级别的TCP服务,使得程序员能更方便地编写服务器端应用程序。通常情况下,服务器端应用程序在启动时将首先绑定本地网络接口的IP地址和端口号,然后进入侦听客户请求的状态,以便于客户端应用程序提出显式请求。

  3. NetworkStream:提供网络访问的基础数据流。一旦侦听到有客户端应用程序请求连接侦听端口,服务器端应用将接受请求,并建立一个负责与客户端应用程序通信的信道。

网络聊天示意图

如下图所示:看似两个在不同网络上的人聊天,实际上都是通过服务端进行接收转发的。

C# 网络编程之简易聊天示例

TCP网络通信示意图

如下图所示:首先是服务端进行监听,当有客户端进行连接时,则建立通讯通道进行通信。

C# 网络编程之简易聊天示例

示例截图

服务端截图,如下所示:

C# 网络编程之简易聊天示例

C# 网络编程之简易聊天示例

客户端截图,如下所示:开启两个客户端,开始美猴王和二师兄的对话。

C# 网络编程之简易聊天示例

C# 网络编程之简易聊天示例

核心代码

发送信息类,如下所示:

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace Common{    /// <summary>    /// 定义一个类,所有要发送的内容,都按照这个来    /// </summary>    public class ChatMessage    {        /// <summary>        /// 头部信息        /// </summary>        public ChatHeader header { get; set; }
        /// <summary>        /// 信息类型,默认为文本        /// </summary>        public ChatType chatType { get; set; }
        /// <summary>        /// 内容信息        /// </summary>        public string info { get; set; }
    }
    /// <summary>    /// 头部信息    /// </summary>    public class ChatHeader    {        /// <summary>        /// id唯一标识        /// </summary>        public string id { get; set; }
        /// <summary>        /// 源:发送方        /// </summary>        public string source { get; set; }
        /// <summary>        /// 目标:接收方        /// </summary>        public string dest { get; set; }
    }
    /// <summary>    /// 内容标识    /// </summary>    public enum ChatMark    {        BEGIN  = 0x0000,        END = 0xFFFF    }
    public enum ChatType {        TEXT=0,        IMAGE=1    }}

打包帮助类,如下所示:所有需要发送的信息,都要进行封装,打包,编码成固定格式,方便解析。

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace Common{    /// <summary>    /// 包帮助类    /// </summary>    public class PackHelper    {        /// <summary>        /// 获取待发送的信息        /// </summary>        /// <param name="text"></param>        /// <returns></returns>        public static byte[] GetSendMsgBytes(string text, string source, string dest)        {            ChatHeader header = new ChatHeader()            {                source = source,                dest = dest,                id = Guid.NewGuid().ToString()            };            ChatMessage msg = new ChatMessage()            {                chatType = ChatType.TEXT,                header = header,                info = text            };            string msg01 = GeneratePack<ChatMessage>(msg);            byte[] buffer = Encoding.UTF8.GetBytes(msg01);            return buffer;        }
        /// <summary>        /// 生成要发送的包        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="t"></param>        /// <returns></returns>        public static string GeneratePack<T>(T t) {            string send = SerializerHelper.JsonSerialize<T>(t);            string res = string.Format("{0}|{1}|{2}",ChatMark.BEGIN.ToString("X").PadLeft(4, '0'), send, ChatMark.END.ToString("X").PadLeft(4, '0'));            int length = res.Length;
            return string.Format("{0}|{1}", length.ToString().PadLeft(4, '0'), res);        }
        /// <summary>        /// 解析包        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="receive">原始接收数据包</param>        /// <returns></returns>        public static T ParsePack<T>(string msg, out string error)        {            error = string.Empty;            int len = int.Parse(msg.Substring(0, 4));//传输内容的长度            string msg2 = msg.Substring(msg.IndexOf("|") + 1);            string[] array = msg2.Split('|');            if (msg2.Length == len)            {                string receive = array[1];                string begin = array[0];                string end = array[2];                if (begin == ChatMark.BEGIN.ToString("X").PadLeft(4, '0') && end == ChatMark.END.ToString("X").PadLeft(4, '0'))                {                    T t = SerializerHelper.JsonDeserialize<T>(receive);                    if (t != null)                    {                        return t;
                    }                    else {                        error = string.Format("接收的数据有误,无法进行解析");                        return default(T);                    }                }                else {                    error = string.Format("接收的数据格式有误,无法进行解析");                    return default(T);                }            }            else {                error = string.Format("接收数据失败,长度不匹配,定义长度{0},实际长度{1}", len, msg2.Length);                return default(T);            }        }    }}

服务端类,如下所示:服务端开启时,需要进行端口监听,等待链接。

using Common;using System;using System.Collections.Generic;using System.Configuration;using System.IO;using System.Linq;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;using System.Threading.Tasks;
/// <summary>/// 描述:MeChat服务端,用于接收数据/// </summary>namespace MeChatServer{    public class Program    {        /// <summary>        /// 服务端IP        /// </summary>        private static string IP;
        /// <summary>        /// 服务端口        /// </summary>        private static int PORT;
        /// <summary>        /// 服务端监听        /// </summary>        private static TcpListener tcpListener;

        public static void Main(string[] args)        {            //初始化信息            InitInfo();            IPAddress ipAddr = IPAddress.Parse(IP);            tcpListener = new TcpListener(ipAddr, PORT);            tcpListener.Start();
            Console.WriteLine("等待连接");            tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");            //如果用户按下Esc键,则结束            while (Console.ReadKey().Key != ConsoleKey.Escape)            {                Thread.Sleep(200);            }            tcpListener.Stop();        }
        /// <summary>        /// 初始化信息        /// </summary>        private static void InitInfo() {            //初始化服务IP和端口            IP = ConfigurationManager.AppSettings["ip"];            PORT = int.Parse(ConfigurationManager.AppSettings["port"]);            //初始化数据池            PackPool.ToSendList = new List<ChatMessage>();            PackPool.HaveSendList = new List<ChatMessage>();            PackPool.obj = new object();        }
        /// <summary>        /// Tcp异步接收函数        /// </summary>        /// <param name="ar"></param>        public static void AsyncTcpCallback(IAsyncResult ar) {            Console.WriteLine("已经连接");            ChatLinker linker = new ChatLinker(tcpListener.EndAcceptTcpClient(ar));            linker.BeginRead();            //继续下一个连接            Console.WriteLine("等待连接");            tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");        }    }}

客户端类,如下所示:客户端主要进行数据的封装发送,接收解析等操作,并在页面关闭时,关闭连接。

using Common;using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Linq;using System.Net.Sockets;using System.Text;using System.Threading;using System.Threading.Tasks;using System.Windows.Forms;
namespace MeChatClient{    /// <summary>    /// 聊天页面    /// </summary>    public partial class FrmMain : Form    {        /// <summary>        /// 链接客户端        /// </summary>        private TcpClient tcpClient;
        /// <summary>        /// 基础访问的数据流        /// </summary>        private NetworkStream stream;
        /// <summary>        /// 读取的缓冲数组        /// </summary>        private byte[] bufferRead;
        /// <summary>        /// 昵称信息        /// </summary>        private Dictionary<string, string> dicNickInfo;
        public FrmMain()        {            InitializeComponent();        }
        private void MainForm_Load(object sender, EventArgs e)        {            //获取昵称            dicNickInfo = ChatInfo.GetNickInfo();            //设置标题            string title = string.Format(":{0}-->{1} 的对话",dicNickInfo[ChatInfo.Source], dicNickInfo[ChatInfo.Dest]);            this.Text = string.Format("{0}:{1}", this.Text, title);            //初始化客户端连接            this.tcpClient = new TcpClient(AddressFamily.InterNetwork);            bufferRead = new byte[this.tcpClient.ReceiveBufferSize];            this.tcpClient.BeginConnect(ChatInfo.IP, ChatInfo.PORT, new AsyncCallback(RequestCallback), null);
        }
        /// <summary>        /// 异步请求链接函数        /// </summary>        /// <param name="ar"></param>        private void RequestCallback(IAsyncResult ar) {            this.tcpClient.EndConnect(ar);            this.lblStatus.Text = "连接服务器成功";            //获取流            stream = this.tcpClient.GetStream();            //先发送一个连接信息            string text = CommonVar.LOGIN;            byte[] buffer = PackHelper.GetSendMsgBytes(text,ChatInfo.Source,ChatInfo.Source);            stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null);            //只有stream不为空的时候才可以读            stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null);        }
        /// <summary>        /// 发送信息        /// </summary>        /// <param name="sender"></param>        /// <param name="e"></param>        private void btnSend_Click(object sender, EventArgs e)        {            string text = this.txtMsg.Text.Trim();            if( string.IsNullOrEmpty(text)){                MessageBox.Show("要发送的信息为空");                return;            }            byte[] buffer = ChatInfo.GetSendMsgBytes(text);            stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null);            this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[ChatInfo.Source]));            this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;            this.rtAllMsg.AppendText(string.Format("\r\n{0}", text));            this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;        }

        /// <summary>        /// 异步读取信息        /// </summary>        /// <param name="ar"></param>        private void ReadMessage(IAsyncResult ar)        {            if (stream.CanRead)            {                int length = stream.EndRead(ar);                if (length >= 1)                {
                    string msg = string.Empty;                    msg = string.Concat(msg, Encoding.UTF8.GetString(bufferRead, 0, length));                    //处理接收的数据                    string error = string.Empty;                    ChatMessage t = PackHelper.ParsePack<ChatMessage>(msg, out error);                    if (string.IsNullOrEmpty(error))                    {                        this.rtAllMsg.Invoke(new Action(() =>                        {                            this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[t.header.source]));                            this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;                            this.rtAllMsg.AppendText(string.Format("\r\n{0}", t.info));                            this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;                            this.lblStatus.Text = "接收数据成功!";                        }));                    }                    else {                        this.lblStatus.Text = "接收数据失败:"+error;                    }                }                //继续读数据                stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null);            }        }
        /// <summary>        /// 发送成功        /// </summary>        /// <param name="ar"></param>        private void WriteMessage(IAsyncResult ar)        {            this.stream.EndWrite(ar);            //发送成功        }
        /// <summary>        /// 页面关闭,断开连接        /// </summary>        /// <param name="sender"></param>        /// <param name="e"></param>        private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)        {            if (MessageBox.Show("正在通话中,确定要关闭吗?", "关闭", MessageBoxButtons.YesNo) == DialogResult.Yes)            {                e.Cancel = false;                string text = CommonVar.QUIT;                byte[] buffer = ChatInfo.GetSendMsgBytes(text);                stream.Write(buffer, 0, buffer.Length);                //发送完成后,关闭连接                this.tcpClient.Close();
            }            else {                e.Cancel = true;            }        }    }}

备注:本示例中,所有的建立连接,数据接收,发送等都是采用异步方式,防止页面卡顿。