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

匿名 (未验证) 提交于 2019-12-02 22:09:29

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

概述

在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("\r\n[{0}]", dicNickInfo[ChatInfo.Source]));  92             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;  93             this.rtAllMsg.AppendText(string.Format("\r\n{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("\r\n[{0}]", dicNickInfo[t.header.source])); 120                             this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left; 121                             this.rtAllMsg.AppendText(string.Format("\r\n{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

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

备注

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!