老温评语:当我 未对网络通讯编程拥有足够深的认识之前,总觉得它非常神秘莫测,觉得它遥之不可及,就像眼巴巴的望着一位美丽的少女而无法进行进一步的活动!呵呵~见笑了。之所以道论此篇是想和网络上的朋友进行一次通讯编程方面的交流,更重要的是是C#语言!我不会用那么所谓的“专假”那样呆板的语言进行描述,我会用最简明最容易理解的意思进行简述,同时也想给新人指明一条学习C#网络编程的捷径,让你们少通几个宵,保养好身体,希望本文能够给你带来抛砖引玉的灵感,最后麻烦您往死里评论,不要给我老温面子! 阅读本文之后您可以将它应用于:游戏开发中的通讯模块、企业ERP、企业客户管理系统、网络聊天系统、C/S软件模型等等网络编程领域,你要相信C#是很强大的! 先打这儿住,如果您的头脑中还没有一点点网络编程的概念的话请先找“百度先生”请他帮你先学习一下!OK,废话不多讲,继续往下看: 要想足够掌握住网络编程,首先要来忽悠一下几个概念: 什么是TCP/UDP协议: TCP协议:是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂。“面向连接”就是在正式通信前必须要与对方建立起连接。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。你不用想象的那么复杂,你就这么认为:TCP协议它是很可靠的,速度会有点慢的,两点或多点之间是有种握手的感觉,互相信任的,好像中间有渠道捆在一起的,这样理解就很容易懂了。 UDP协议:是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!我们如果把TCP比喻成打电话,那么UDP就是发传统的信件,发过去再说,管它死活,我只要我自己快捷高效就行。ping”命令”就是一个很好的应用。 TCP就像你的老婆,很可靠在家里帮你料理一切家务,而UDP就像你的情人,让你着迷让你的工作变得高效,让你的荷尔蒙变的高效,让你的金钱消费变的高效,同时也让你背负了意外险~~呵呵! 什么是SOCKET: 你经常听到人们谈论着“socket”,或许你还不知道它的确切含义。现在让我告诉你:它是使用标准Unix文件描述符(filedescriptor)和其它程序通讯的方式。什么?你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实:Unix程序在执行任何形式的I/O的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。但是(注意后面的话),这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix中所有的东西就是文件!所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。 就此打住,我不想再多说过多的SOCKET概念,说白了就是一个概念,它是一种概念,系统为了实现这个概念提供了一个API接口给我们使用罢了,一切交给系统底层处理了。 什么是ISO/OSI参考模型: 什么?我晕,ISO/OSI 这是啥,呵呵。这个知识我认为有必要了解,什么全国计算机等级考试啦,程序员考试啦,软件设计师考试啦都会考到这个知识点。 有必要看看懂!一幅图搞定它,用不着去背这些概念,只要知道一些服务存在于哪层上面就可以了。  什么是多线程技术 相信大家在使用电脑的时候已经接触过这个名词,所谓的多线程用简单话来说就是在同一时间处理多项任务。那么如此说来无论在现实生活中还是在电脑上都存在着多线程(multithreading)这个说法,因此同样的是在现实生活中还是在电脑上通过多线程将可以有效提供工作效率。 这里给大家举个例子,比如你正在为家人准备晚餐,今天的晚餐将会由下面三道菜:烤鸡肉、土豆泥以及四季豆。如果打算以“单线程”的方式做这顿晚餐,那么你也许会首先把烤鸡肉做好,做好烤鸡肉之后再做土豆泥然后是四季豆。不幸的是当你把最后一道菜四季豆做好之后,你就会发现前面两道菜烤鸡肉和土豆泥早已经冷掉了。 如果你以“多线程”的 方式做这顿晚餐又会是什么样的情形呢?比如你可以煮开水的同时把鸡肉从冰箱里取出来开始化冻并把烤箱打开进行预温,这两步是同步进行的。接下来,你可以选 择开始给土豆削皮,当你削好皮之后你会发现这个时候水正好烧开了,这时你可以将削好的土豆放进开水里煮。与此同时你的鸡肉也已准备好同时烤箱也准备就序, 这时你的烤鸡肉的操作也就可以同步进行了。由于四季豆并不需要花费你太长的时间,因此这个时候你可以把四季豆洗干净然后放在一边。等土豆快要煮好的时候, 你也可以将四季豆放在锅里面一起蒸熟了。最后,把所有的东西装盘,晚上餐三道菜就此结束。 以上的语言我可以这样概括它:我吃了二个面包,不感觉饱,但是我吃最后个面包的时候就会明显感觉到它的存在! 下面我们继续吃最重要部分,最后个面包。 我用C#实现了一个多客户端与服务端通讯的小工具,效果图如下:  涉及到的源码将开放下载! 它由server端和Client端组成,典型的C/S结构例子.这个例子很简单,但是一应俱全,首先我们来看服务端的代码部分: 因为我们会用到Socket编程和线程技术,所以要引入下面三个命名空间 using System.Net.Sockets; using System.Net; using System.Threading; 完整的代码为: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Net.Sockets; using System.Net; using System.Threading; using System.Collections; namespace mytcpserver  { public partial class Form1 : Form  {  Socket s;  IPEndPoint ServerIPEP; private Thread th;  Socket uc; private ArrayList alSock; // public Form1()  {  InitializeComponent();  Control.CheckForIllegalCrossThreadCalls = false;  } public void Server()  { int port = 5656;  ServerIPEP = new IPEndPoint(IPAddress.Any, port);  s = new Socket(ServerIPEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);  s.Bind((EndPoint)ServerIPEP);  s.Listen( 10);  alSock = new ArrayList(); while ( true)  { try  {  uc = s.Accept();  alSock.Add(uc); this.textBox1.AppendText(System.Convert.ToString(uc)); byte[] data = new byte[ 2048]; int rect = uc.Receive(data); byte[] chat = new byte[rect];  Buffer.BlockCopy(data, 0, chat, 0, rect); this.mymessage.AppendText( "["+uc.RemoteEndPoint.ToString() + "]"+ System.Text.Encoding.Default.GetString(chat));  } catch (Exception ex)  { // MessageBox.Show(ex.Message);  }  }  } private void Form1_Load( object sender, EventArgs e)  {  } private void button1_Click( object sender, EventArgs e)  { //开始监听部分 this.button1.Enabled = false; this.button2.Enabled = true;  th = new Thread( new ThreadStart(Server)); //新建一个用于监听的线程  th.Start(); //打开新线程  } private void button2_Click( object sender, EventArgs e)  { //停止监听部分 try  { this.button2.Enabled = false; this.button1.Enabled = true;  s.Close();  th.Abort(); //终止线程  } catch (Exception ex)  { // MessageBox.Show(ex.Message);  }  } private void button3_Click( object sender, EventArgs e)  { //回送消息部分 if (uc == null)  {  MessageBox.Show( "我说你要发给谁啊.");  } else  { int index = int.Parse( this.textBox2.Text.Trim());  Socket sc = (Socket)alSock[index]; //发送数据  sc.Send(System.Text.Encoding.Default.GetBytes( this.answermessage.Text.Trim()));  }  }  }  } 我不想对上面的代码部分讲更多的解释,如果你有不明白的问题跟贴留言,上面的这代码还是很容易理解的,注:这儿使用的是单线程技术 下面我们再来看客户端的代码: 相对而言客户端的代码更加简单 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Net.Sockets; using System.Net; using System.Threading; namespace mytcpchat  { public partial class Form1 : Form  { private Thread th;  Socket c; public Form1()  {  InitializeComponent();  Control.CheckForIllegalCrossThreadCalls = false;  } private void Form1_Load( object sender, EventArgs e)  {  } public void Client()  { try  {  IPEndPoint ServerIPEP = new IPEndPoint(IPAddress.Parse( this.comboBox1.Text.Trim()), 5656);  c = new Socket(ServerIPEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);  c.Connect((EndPoint)ServerIPEP);  c.Send(System.Text.Encoding.Default.GetBytes( this.sendmessage.Text.Trim())); byte[] data = new byte[ 2048]; while ( true)  { int rect = c.Receive(data); byte[] chat = new byte[rect];  Buffer.BlockCopy(data, 0, chat, 0, rect); this.message.AppendText(System.Text.Encoding.Default.GetString(chat));  }  } catch (Exception ex)  { // MessageBox.Show(ex.Message);  }  } private void button1_Click( object sender, EventArgs e)  { try  {  th = new Thread( new ThreadStart(Client)); //新建一个用于监听的线程  th.Start(); //打开新线程  } catch (Exception ex)  {  MessageBox.Show(ex.Message);  }  }  }  } OK,你现在可能会说,老温给我源代码吧 注意:你阅读上面部分之后,你能解决的网络环境还处于A网段到B网段是直通的情况,没有双方处于内网中,也就是发起的一方必须知道S端的公网IP地址,那么我两个网络可能存在两个局域网中会怎么样呢?不是发不通了,下面我们来继续解决这个问题,先来讲一下打洞的概念,比较枯燥,但是为了成功的喜悦值得! 网络上的大多数UDP原理研究分析后,自己总结了下面的结果. 1、client A 登录 通过http服务器 webservices 验证成功获取自身信息与好友列表信息等。 2、Http服务器登记client A的NAT后的IP地址与端口 3、Client B登录 通过http服务器 webservices 验证成功获取自身信息与好友列表信息等。 4、Http服务器登记client B的NAT后的IP地址与端口 5、Client A想发送消息给Client B,向HTtp服务器获取Client B的在线IP地址 6、Client A获得Client B的IP地址后并发送UDP信息到Client B 7、Client A与Client B请求失败,信息丢失,此时Client A报告Http服务器要求服务器帮忙对Client B进行通知 8、Http服务器接到此命令后,将Client A的IP地址发给Client B,要求他连接 9、Client B收到HTTP服务器的信息后发送请求到 Client A 10、由于此时Client A NAT已经存在Clinet B的session,所以此时 Client A与Client B建立链接成功。 11、Client A发送消息到 Client B成功,不经HTTP服务器中转 总结:换句话说就是HTTP服务器启动了“和事老”的功能。为不信任的两个人发出命令从而使他们达到了一种互相承认而此建立链接关系 其中最主要的是Server 的一个方法就是发送消息到被打洞的客户端 客户端发送消息需要判断该消息是否发送成功,不成功则要求请求打洞.若成功则不需要要求打洞!客户端在接收到服务器的打洞指令后,则作出UDP发送响应 接下来我们一起来利用一个例子进行学习:p2pDemo工程  这是一个基于控制台应用程序。该代码乃本人一位朋友编写,故代码将不贴出来,如果学习的可以下载进行学习,然后在此提出留言进行讨论! 本人强烈推荐您学习,你不管搞C#/C++/JAVA,学习的是它的打洞原理 在本人参与的项目应用中,既然我们在编写通讯程序,加密与解密我们一定要考虑进去。 首先我们来看加密与解密部分: 公钥加密私钥解密, 没问题,也可以说是"公共密钥加密系统" 私钥加密公钥解密,一般不这么说,应叫"私钥签名,公钥验证",也可以说是“公共密钥签名系统” 再来说一下"公共密钥签名系统"目的:(如果晕就多看几遍,这个没搞清,后面的代码就更晕) A欲传(信息)给B,但又怕B不确信该信息是A发的。 1.A选计算(信息)的HASH值,如用MD5方式计算,得到:[MD5(信息)] 2.然后用自已的私钥加密HASH值,得到:[私钥(MD5(信息))] 3.最后将信息与密文一起传给B:传给B:[(信息) + 私钥(MD5(信息))] B接到 :[(信息) + 私钥(MD5(信息))] 1.先用相同的HASH算法算出(信息)的HASH值,这里也使用MD5方式 得到: [MD5(信息)!] 2. 再用A的公钥解密 [ 私钥(MD5(信息))] [公钥(私钥(MD5(信息)))] = [(MD5(信息)] 如能解开,证明该 [ 私钥(MD5(信息))]是A发送的 3.再比效[MD5(信息)!]与[(MD5(信息)] 如果相同,表示(信息)在传递过程中没有被他人修改过。 下面我们再来讲一下数字签名技术: 我们对加解密算法已经有了一定理解,可以进一步讨论"数字签名"(注意不要与数字认证混淆)的问题了,即 如何给一个计算机文件进行签字。数字签字可以用对称算法实现,也可以用公钥算法实现。但前者除了文件签字者和文件接受者双方,还需要第三方认证,较麻烦; 通过公钥加密算法的实现方法,由于用秘密密钥加密的文件,需要靠公开密钥来解密,因此这可以作为数字签名,签名者用秘密密钥加密一个签名(可以包括姓名、 证件号码、短信息等信息),接收人可以用公开的、自己的公开密钥来解密,如果成功,就能确保信息来自该公开密钥的所有人。 公钥密码体制实现数字签名的基本原理很简单,假设A要发送一个电子文件给B,A、B双方只需经过下面三个步骤即可: 1. A用其私钥加密文件,这便是签字过程 2. A将加密的文件送到B 3. B用A的公钥解开A送来的文件 这样的签名方法是符合可靠性原则的。即: 签字是可以被确认的, 签字是无法被伪造的, 签字是无法重复使用的, 文件被签字以后是无法被篡改的, 签字具有无可否认性, 数字签名就是通过一个单向函数对要传送的报文进行处理得到的用以认证报文来源并核实报文是否发生变化的一个字母数字串。用这几个字符串来代替书写签名或印章,起到与书写签名或印章同样的法律效用。国际社会已开始制定相应的法律、法规,把数字签名作为执法的依据。 我想读者最关注的是如果利用C#进行这方面的编程,由于种种原因本人不能将项目中的代码全部公开,将原理重要点公布,如下: 序列化类: using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace common  { public class SerializeData  { public static byte[] Serialize( object obj)  {  BinaryFormatter binaryF = new BinaryFormatter();  MemoryStream ms = new MemoryStream( 1024 * 10);  binaryF.Serialize(ms, obj);  ms.Seek( 0, SeekOrigin.Begin); byte[] buffer = new byte[( int)ms.Length];  ms.Read(buffer, 0, buffer.Length);  ms.Close(); return buffer;  } public static object Deserialize( byte[] buffer)  {  BinaryFormatter binaryF = new BinaryFormatter();  MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length, false); object obj = binaryF.Deserialize(ms);  ms.Close(); return obj;  }  }  }  TDES加密算法,注意加密的长度,一定要多测,注意双字节!害了本人不少天 using System; using System.Collections.Generic; using System.Text; using System.Net.Sockets; using System.IO; using System.Security.Cryptography; using System.Text.RegularExpressions; namespace common  { public class TripleDES  { public static int GetStrLength( string content)  { int length = 0; for ( int i = 0; i < content.Length; i++)  { if (Regex.IsMatch(content[i].ToString(), @"[\一-\龥]+") || Regex.IsMatch(content[i].ToString(), @"[^\x00-\x7F]+"))  {  length += 2;  } else  {  length++;  }  } return length;  } //MD5散列方法 public string MD5( string content)  {  MD5 md5 = new MD5CryptoServiceProvider(); byte[] bt = Encoding.Default.GetBytes(content); //将待加密字符转为 字节型数组 byte[] resualt = md5.ComputeHash(bt); //将字节数组转为加密的字节数组 return BitConverter.ToString(resualt).Replace( "-", ""); //将数字转为string 型去掉内部的无关字符  } //使用对称加密加密字符串 public byte[] EncryptText( string str, byte[] Key, byte[] IV)  { //创建一个内存流  MemoryStream memoryStream = new MemoryStream(); //使用传递的密钥和IV创建加密流  CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), CryptoStreamMode.Write); //将传递的字符串转换为字数组 byte[] toEncrypte = Encoding.Default.GetBytes(str); try  { //将字节数组写入加密流,并清除缓冲区  cryptoStream.Write(toEncrypte, 0, toEncrypte.Length);  cryptoStream.FlushFinalBlock(); //得到加密后的字节数组 byte[] encryptedBytes = memoryStream.ToArray(); return encryptedBytes;  } catch  { //SetListBox("加密出错:" + err.Message); return null;  } finally  {  cryptoStream.Close();  memoryStream.Close();  }  } //使用对称加密算法,解密接收的字符串 public string DecryptText( byte[] dataBytes, byte[] Key, byte[] IV, int datalength)  { //根据加密后的字节数组创建五个内存流  MemoryStream memoryStream = new MemoryStream(dataBytes); //使用传递的密钥、IV和内存流创建解密流  CryptoStream cryptoStream = new CryptoStream(memoryStream, new TripleDESCryptoServiceProvider().CreateDecryptor(Key, IV), CryptoStreamMode.Read); //创建一个字节数组保存解密后的数据 byte[] decryptBytes = new byte[datalength]; try  { //从解密流中将解密后的数据读到字节数组中  cryptoStream.Read(decryptBytes, 0, decryptBytes.Length); //得到解密后的字符串 string decryptedString = Encoding.Default.GetString(decryptBytes); return decryptedString;  } catch  { // SetListBox("解密出错" + err.Message); return null;  } finally  {  cryptoStream.Close();  memoryStream.Close();  }  }  }  }  注意:在S端进行多线程编写时,一定要考虑临界资源,以及线程中对界面上的控件的操作,在C#中是不允许的,可以用委托来实现它。 题外话:在现有的游戏开发中或大规则性能要求场合,还会有更好的线程模型,比如IOCP,EPOLL模型等!它是专门用来处理这方面的一种模型。 这类文章特别难写,因为作为学习者不仅要花时间去读而且更重要的是要读的懂,我写的可能够烂的,我得去拜见我小学的语文老师了..... 好了,本文中提到的一些源代码下载地址为: 老温网络编程源码下载 http://www.cnblogs.com/Files/wenweifeng/老温网络编程.rar 解压密码为:laowen
|