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


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

    概述

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

    涉及知识点

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

    1. TcpClient : TcpClient类为TCP网络服务提供客户端连接,它构建于Socket类之上,以提供较高级别的TCP服务,提供了通过网络连接、发送和接收数据的简单方法。
    2. TcpListener:构建于Socket之上,提供了更高抽象级别的TCP服务,使得程序员能更方便地编写服务器端应用程序。通常情况下,服务器端应用程序在启动时将首先绑定本地网络接口的IP地址和端口号,然后进入侦听客户请求的状态,以便于客户端应用程序提出显式请求。
    3. NetworkStream:提供网络访问的基础数据流。一旦侦听到有客户端应用程序请求连接侦听端口,服务器端应用将接受请求,并建立一个负责与客户端应用程序通信的信道。

    网络聊天示意图

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

    TCP网络通信示意图

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

    示例截图

    服务端截图,如下所示:

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

    核心代码

    发送信息类,如下所示:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 
     7 namespace Common
     8 {
     9     /// <summary>
    10     /// 定义一个类,所有要发送的内容,都按照这个来
    11     /// </summary>
    12     public class ChatMessage
    13     {
    14         /// <summary>
    15         /// 头部信息
    16         /// </summary>
    17         public ChatHeader header { get; set; }
    18 
    19         /// <summary>
    20         /// 信息类型,默认为文本
    21         /// </summary>
    22         public ChatType chatType { get; set; }
    23 
    24         /// <summary>
    25         /// 内容信息
    26         /// </summary>
    27         public string info { get; set; }
    28 
    29     }
    30 
    31     /// <summary>
    32     /// 头部信息
    33     /// </summary>
    34     public class ChatHeader
    35     {
    36         /// <summary>
    37         /// id唯一标识
    38         /// </summary>
    39         public string id { get; set; }
    40 
    41         /// <summary>
    42         /// 源:发送方
    43         /// </summary>
    44         public string source { get; set; }
    45 
    46         /// <summary>
    47         /// 目标:接收方
    48         /// </summary>
    49         public string dest { get; set; }
    50 
    51     }
    52 
    53     /// <summary>
    54     /// 内容标识
    55     /// </summary>
    56     public enum ChatMark
    57     {
    58         BEGIN  = 0x0000,
    59         END = 0xFFFF
    60     }
    61 
    62     public enum ChatType {
    63         TEXT=0,
    64         IMAGE=1
    65     }
    66 }
    View Code

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

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 
     7 namespace Common
     8 {
     9     /// <summary>
    10     /// 包帮助类
    11     /// </summary>
    12     public class PackHelper
    13     {
    14         /// <summary>
    15         /// 获取待发送的信息
    16         /// </summary>
    17         /// <param name="text"></param>
    18         /// <returns></returns>
    19         public static byte[] GetSendMsgBytes(string text, string source, string dest)
    20         {
    21             ChatHeader header = new ChatHeader()
    22             {
    23                 source = source,
    24                 dest = dest,
    25                 id = Guid.NewGuid().ToString()
    26             };
    27             ChatMessage msg = new ChatMessage()
    28             {
    29                 chatType = ChatType.TEXT,
    30                 header = header,
    31                 info = text
    32             };
    33             string msg01 = GeneratePack<ChatMessage>(msg);
    34             byte[] buffer = Encoding.UTF8.GetBytes(msg01);
    35             return buffer;
    36         }
    37 
    38         /// <summary>
    39         /// 生成要发送的包
    40         /// </summary>
    41         /// <typeparam name="T"></typeparam>
    42         /// <param name="t"></param>
    43         /// <returns></returns>
    44         public static string GeneratePack<T>(T t) {
    45             string send = SerializerHelper.JsonSerialize<T>(t);
    46             string res = string.Format("{0}|{1}|{2}",ChatMark.BEGIN.ToString("X").PadLeft(4, '0'), send, ChatMark.END.ToString("X").PadLeft(4, '0'));
    47             int length = res.Length;
    48 
    49             return string.Format("{0}|{1}", length.ToString().PadLeft(4, '0'), res);
    50         }
    51 
    52         /// <summary>
    53         /// 解析包
    54         /// </summary>
    55         /// <typeparam name="T"></typeparam>
    56         /// <param name="receive">原始接收数据包</param>
    57         /// <returns></returns>
    58         public static T ParsePack<T>(string msg, out string error)
    59         {
    60             error = string.Empty;
    61             int len = int.Parse(msg.Substring(0, 4));//传输内容的长度
    62             string msg2 = msg.Substring(msg.IndexOf("|") + 1);
    63             string[] array = msg2.Split('|');
    64             if (msg2.Length == len)
    65             {
    66                 string receive = array[1];
    67                 string begin = array[0];
    68                 string end = array[2];
    69                 if (begin == ChatMark.BEGIN.ToString("X").PadLeft(4, '0') && end == ChatMark.END.ToString("X").PadLeft(4, '0'))
    70                 {
    71                     T t = SerializerHelper.JsonDeserialize<T>(receive);
    72                     if (t != null)
    73                     {
    74                         return t;
    75 
    76                     }
    77                     else {
    78                         error = string.Format("接收的数据有误,无法进行解析");
    79                         return default(T);
    80                     }
    81                 }
    82                 else {
    83                     error = string.Format("接收的数据格式有误,无法进行解析");
    84                     return default(T);
    85                 }
    86             }
    87             else {
    88                 error = string.Format("接收数据失败,长度不匹配,定义长度{0},实际长度{1}", len, msg2.Length);
    89                 return default(T);
    90             }
    91         }
    92     }
    93 }
    View Code

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

     1 using Common;
     2 using System;
     3 using System.Collections.Generic;
     4 using System.Configuration;
     5 using System.IO;
     6 using System.Linq;
     7 using System.Net;
     8 using System.Net.Sockets;
     9 using System.Text;
    10 using System.Threading;
    11 using System.Threading.Tasks;
    12 
    13 /// <summary>
    14 /// 描述:MeChat服务端,用于接收数据
    15 /// </summary>
    16 namespace MeChatServer
    17 {
    18     public class Program
    19     {
    20         /// <summary>
    21         /// 服务端IP
    22         /// </summary>
    23         private static string IP;
    24 
    25         /// <summary>
    26         /// 服务端口
    27         /// </summary>
    28         private static int PORT;
    29 
    30         /// <summary>
    31         /// 服务端监听
    32         /// </summary>
    33         private static TcpListener tcpListener;
    34 
    35 
    36         public static void Main(string[] args)
    37         {
    38             //初始化信息
    39             InitInfo();
    40             IPAddress ipAddr = IPAddress.Parse(IP);
    41             tcpListener = new TcpListener(ipAddr, PORT);
    42             tcpListener.Start();
    43           
    44             Console.WriteLine("等待连接");
    45             tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");
    46             //如果用户按下Esc键,则结束
    47             while (Console.ReadKey().Key != ConsoleKey.Escape)
    48             {
    49                 Thread.Sleep(200);
    50             }
    51             tcpListener.Stop();
    52         }
    53 
    54         /// <summary>
    55         /// 初始化信息
    56         /// </summary>
    57         private static void InitInfo() {
    58             //初始化服务IP和端口
    59             IP = ConfigurationManager.AppSettings["ip"];
    60             PORT = int.Parse(ConfigurationManager.AppSettings["port"]);
    61             //初始化数据池
    62             PackPool.ToSendList = new List<ChatMessage>();
    63             PackPool.HaveSendList = new List<ChatMessage>();
    64             PackPool.obj = new object();
    65         }
    66 
    67         /// <summary>
    68         /// Tcp异步接收函数
    69         /// </summary>
    70         /// <param name="ar"></param>
    71         public static void AsyncTcpCallback(IAsyncResult ar) {
    72             Console.WriteLine("已经连接");
    73             ChatLinker linker = new ChatLinker(tcpListener.EndAcceptTcpClient(ar));
    74             linker.BeginRead();
    75             //继续下一个连接
    76             Console.WriteLine("等待连接");
    77             tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");
    78         }
    79     }
    80 }
    View Code

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

      1 using Common;
      2 using System;
      3 using System.Collections.Generic;
      4 using System.ComponentModel;
      5 using System.Data;
      6 using System.Drawing;
      7 using System.Linq;
      8 using System.Net.Sockets;
      9 using System.Text;
     10 using System.Threading;
     11 using System.Threading.Tasks;
     12 using System.Windows.Forms;
     13 
     14 namespace MeChatClient
     15 {
     16     /// <summary>
     17     /// 聊天页面
     18     /// </summary>
     19     public partial class FrmMain : Form
     20     {
     21         /// <summary>
     22         /// 链接客户端
     23         /// </summary>
     24         private TcpClient tcpClient;
     25 
     26         /// <summary>
     27         /// 基础访问的数据流
     28         /// </summary>
     29         private NetworkStream stream;
     30 
     31         /// <summary>
     32         /// 读取的缓冲数组
     33         /// </summary>
     34         private byte[] bufferRead;
     35 
     36         /// <summary>
     37         /// 昵称信息
     38         /// </summary>
     39         private Dictionary<string, string> dicNickInfo;
     40 
     41         public FrmMain()
     42         {
     43             InitializeComponent();
     44         }
     45 
     46         private void MainForm_Load(object sender, EventArgs e)
     47         {
     48             //获取昵称
     49             dicNickInfo = ChatInfo.GetNickInfo();
     50             //设置标题
     51             string title = string.Format(":{0}-->{1} 的对话",dicNickInfo[ChatInfo.Source], dicNickInfo[ChatInfo.Dest]);
     52             this.Text = string.Format("{0}:{1}", this.Text, title);
     53             //初始化客户端连接
     54             this.tcpClient = new TcpClient(AddressFamily.InterNetwork);
     55             bufferRead = new byte[this.tcpClient.ReceiveBufferSize];
     56             this.tcpClient.BeginConnect(ChatInfo.IP, ChatInfo.PORT, new AsyncCallback(RequestCallback), null);
     57           
     58         }
     59 
     60         /// <summary>
     61         /// 异步请求链接函数
     62         /// </summary>
     63         /// <param name="ar"></param>
     64         private void RequestCallback(IAsyncResult ar) {
     65             this.tcpClient.EndConnect(ar);
     66             this.lblStatus.Text = "连接服务器成功";
     67             //获取流
     68             stream = this.tcpClient.GetStream();
     69             //先发送一个连接信息
     70             string text = CommonVar.LOGIN;
     71             byte[] buffer = PackHelper.GetSendMsgBytes(text,ChatInfo.Source,ChatInfo.Source);
     72             stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null);
     73             //只有stream不为空的时候才可以读
     74             stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null);
     75         }
     76 
     77         /// <summary>
     78         /// 发送信息
     79         /// </summary>
     80         /// <param name="sender"></param>
     81         /// <param name="e"></param>
     82         private void btnSend_Click(object sender, EventArgs e)
     83         {
     84             string text = this.txtMsg.Text.Trim();
     85             if( string.IsNullOrEmpty(text)){
     86                 MessageBox.Show("要发送的信息为空");
     87                 return;
     88             }
     89             byte[] buffer = ChatInfo.GetSendMsgBytes(text);
     90             stream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteMessage), null);
     91             this.rtAllMsg.AppendText(string.Format("
    [{0}]", dicNickInfo[ChatInfo.Source]));
     92             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;
     93             this.rtAllMsg.AppendText(string.Format("
    {0}", text));
     94             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;
     95         }
     96 
     97     
     98         /// <summary>
     99         /// 异步读取信息
    100         /// </summary>
    101         /// <param name="ar"></param>
    102         private void ReadMessage(IAsyncResult ar)
    103         {
    104             if (stream.CanRead)
    105             {
    106                 int length = stream.EndRead(ar);
    107                 if (length >= 1)
    108                 {
    109 
    110                     string msg = string.Empty;
    111                     msg = string.Concat(msg, Encoding.UTF8.GetString(bufferRead, 0, length));
    112                     //处理接收的数据
    113                     string error = string.Empty;
    114                     ChatMessage t = PackHelper.ParsePack<ChatMessage>(msg, out error);
    115                     if (string.IsNullOrEmpty(error))
    116                     {
    117                         this.rtAllMsg.Invoke(new Action(() =>
    118                         {
    119                             this.rtAllMsg.AppendText(string.Format("
    [{0}]", dicNickInfo[t.header.source]));
    120                             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;
    121                             this.rtAllMsg.AppendText(string.Format("
    {0}", t.info));
    122                             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;
    123                             this.lblStatus.Text = "接收数据成功!";
    124                         }));
    125                     }
    126                     else {
    127                         this.lblStatus.Text = "接收数据失败:"+error;
    128                     }
    129                 }
    130                 //继续读数据
    131                 stream.BeginRead(bufferRead, 0, bufferRead.Length, new AsyncCallback(ReadMessage), null);
    132             }
    133         }
    134 
    135         /// <summary>
    136         /// 发送成功
    137         /// </summary>
    138         /// <param name="ar"></param>
    139         private void WriteMessage(IAsyncResult ar)
    140         {
    141             this.stream.EndWrite(ar);
    142             //发送成功
    143         }
    144 
    145         /// <summary>
    146         /// 页面关闭,断开连接
    147         /// </summary>
    148         /// <param name="sender"></param>
    149         /// <param name="e"></param>
    150         private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
    151         {
    152             if (MessageBox.Show("正在通话中,确定要关闭吗?", "关闭", MessageBoxButtons.YesNo) == DialogResult.Yes)
    153             {
    154                 e.Cancel = false;
    155                 string text = CommonVar.QUIT;
    156                 byte[] buffer = ChatInfo.GetSendMsgBytes(text);
    157                 stream.Write(buffer, 0, buffer.Length);
    158                 //发送完成后,关闭连接
    159                 this.tcpClient.Close();
    160 
    161             }
    162             else {
    163                 e.Cancel = true;
    164             }
    165         }
    166     }
    167 }
    View Code

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

    源码下载链接

    备注

    每一次的努力,都是幸运的伏笔。

  • 相关阅读:
    day3-python之函数进阶(三)
    day3-python之函数初识(二)
    day3-python之文件操作(一)
    tomcat
    集群
    nginx
    nginx--zabbix监控status waiting
    zabbix监控之mysql主从状态&mysql主从延迟
    zabbix监控之进程&日志监控
    zabbix监控流程(监控linux上某个文件是否有改动)
  • 原文地址:https://www.cnblogs.com/hsiang/p/11530691.html
Copyright © 2020-2023  润新知