还记得刚刚开始接触编程开发时,傻傻的将网站开发和网络编程混为一谈,常常因分不清楚而引为笑柄。后来勉强分清楚,又因为各种各样的协议端口之类的名词而倍感神秘,所以为了揭开网络编程的神秘面纱,本文尝试以一个简单的小例子,简述在网络编程开发中涉及到的相关知识点,仅供学习分享使用,如有不足之处,还请指正。
概述
在TCP/IP协议族中,传输层主要包括TCP和UDP两种通信协议,它们以不同的方式实现两台主机中的不同应用程序之间的数据传输,即数据的端到端传输。由于它们的实现方式不同,因此各有一套属于自己的端口号,且相互独立。采用五元组(协议,信源机IP地址,信源应用进程端口,信宿机IP地址,信宿应用进程端口)来描述两个应用进程之间的通信关联,这也是进行网络程序设计最基本的概念。传输控制协议(Transmission Control Protocol,TCP)提供一种面向连接的、可靠的数据传输服务,保证了端到端数据传输的可靠性。
涉及知识点
本例中涉及知识点如下所示:
- TcpClient : TcpClient类为TCP网络服务提供客户端连接,它构建于Socket类之上,以提供较高级别的TCP服务,提供了通过网络连接、发送和接收数据的简单方法。
- TcpListener:构建于Socket之上,提供了更高抽象级别的TCP服务,使得程序员能更方便地编写服务器端应用程序。通常情况下,服务器端应用程序在启动时将首先绑定本地网络接口的IP地址和端口号,然后进入侦听客户请求的状态,以便于客户端应用程序提出显式请求。
- 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 }
打包帮助类,如下所示:所有需要发送的信息,都要进行封装,打包,编码成固定格式,方便解析。
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 }
服务端类,如下所示:服务端开启时,需要进行端口监听,等待链接。
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 }
客户端类,如下所示:客户端主要进行数据的封装发送,接收解析等操作,并在页面关闭时,关闭连接。
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 }
备注:本示例中,所有的建立连接,数据接收,发送等都是采用异步方式,防止页面卡顿。
源码下载链接
备注
每一次的努力,都是幸运的伏笔。