C#的坑和需特别注意的问题的集合


以下均为Windows环境用VS调试出的结果。

网络编程篇:

TCP子篇:

  1、服务型Socket执行sockServ.BeginAccept(AcceptedFeedback, sockServ)时;如果此时另一个地方执行sockServ.Close(),则不管是否接收到了连接请求,都会立刻执行AcceptedFeedback回调函数(或说sockServ.Close()会造成sockServ.BeginAccept()成功);不是很清楚这里的道理。

  2、服务型Socket在sockServ.Accept(...)或BeginAccept(...)返回的toClient;它们的LocalEndPoint是一样的,也就是说sockServ和toClient是共用一个IP和端口。

  3、Socket对象可以多次Close()不会异常,且每次Close()后会将Connected重置为false。

  4、Socket对象的Send和BeginSend是容易让人迷惑的(或说Send和BeginSend写的不好),这里只说sock.BeginSend(msgBuffer, 0, msgBuffer.Length, SocketFlags.None, SendedFeedback, sock);连接成功情况下哪怕是服务端没有接收数据它也会发送成功(这种方式感觉像UDP一样)且调用SendedFeedback回调函数,且该回调函数内部sock.EndSend()也能成功返回 已发送字节数

  5、和4有点关联,当sockServToClient和sockClient建立连接后,正常情况下比如sockClient往sockServToClient中发送数据是一定能发送成功的,且sockClient发送的数据是存在了sockServToClient这边(可能有个缓存池),sockServToClient.Receive(...)实际上是从本地获取数据而不是发个信号给sockClient,让它把数据发送过来。也就是说连接正常情况下client给serv发消息,消息存在了servToClient这边,然后client断开连接,此时servToClient.Receive(...)是能收到数据的(但当数据量很大的时候不敢保证能收全:未测试)。

  6、sockServ.BeginAccept(...)后返回一个和客户端交互的Socket: toClient,这里要注意,如果将sockServ.Close()是不会影响toClient的,toClient仍然可以正常收发消息。

  7、正常连接情况下servToClient.BeginReceive(...)在等待客户端数据的过程中,如果客户端那边异常关闭连接(比如客户端程序被强制杀死),而不是通过Close()等方法关闭连接,则服务端即servToClient.BeginReceive(...)会发生异常。

  8、正常连接情况下servToClient.BeginReceive(...)在等待客户端数据的过程中,如果客户端那边主动关闭连接(比如Close),则servToClient.BeginReceive(...)会执行成功但是收到的是0字节数据(没出异常则一定能收到这条0字节结束型消息),且会调用ReceiveFeedback回调函数且servToClient.Connected为true,且尽管客户端已经Close(),但是ReceiveCallback中的EndReceive(iar)却不会有异常。其实应该这么讲,客户端正常关闭后,BeginReceive()会一直执行成功,但是每次收到的都是0字节,且Connected为true,EndReceive不会出问题。如果说Connected为true时 EndReceive()出问题了(如收到远程主机强迫关闭了连接,即异常关闭连接)那很有可能是进入回调函数ReceiveCallback之后(Connected哪怕对方异常关闭貌似也是true),执行EndReceive之前客户端Socket异常关闭。

  9、正常连接情况下servToClient.BeginReceive(...)在等待客户端数据的过程中,客户端那边数据一直没来而servToClient这边执行servToClient.Close();则servToClient.BeginReceive(...)会马上执行成功但是ReceiveFeedback回调函数里判断servToClient.Connected是false

  10、代码:

  var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  
// 注意:client.LocalEndPoint及RemoteEndPoint都是在连接成功的前提下才有值的,然而这里是异步,所以存在这种可能:
  
// 即client还没连接成功(但是已经开始连接),它有个默认的地址 00:00:00:00:48888,而等它连接成功后这个值就变了,所以会出现找不到Key的情况
  client.BeginConnect(new IPEndPoint(_servIp, _port), ConnectedCallback, client);

    上面的代码在只执行了第一行代码时,client的LocalEndPoint和RemoteEndPoint都只是null,当执行了第二行代码时,此时分两种情况:

    情况一:已经开始连接但是还没有连接成功时,此时RemoteEndPoint是一个异常(应该是,反正肯定不是一个正确值),而LocalEndPoint则会分配好端口,但是IP却是0.0.0.0:PortNum。

    情况二:经历情况一这一过程后连接成功并且会执行相应的回调函数(假设有的话),此时RemoteEndPoint和LocalEndPoint的值都是正确的数据

    结论:如果有需要将LocalEndPoint.ToString()作为Key,不能将Add(Key,Value)的代码写在BeginConnect(...)前后,而应该是写在连接成功后的回调函数(或者客户端的Socket也主动Bind(...)),否则Key可能不符设计意愿。

  11、BeginConnect()系统没有提供连接超时时间的设置,它大概的默认时间是20-30秒,也就是说没有连接上但是时间从开始连接超过了25秒左右会执行ConnectedCallback回调函数(此处待验证),故里面需要判断client.Connected。

  12、TCP和UDP在IP一样时也可以用同一个“端口”号,因为它们协议不同;端口的本质也是为了区分进程,而协议也是区分进程的一部分。

  13、服务端在执行ReceiveCallback中的代码时在没有toClient.Close()之前(这里具体异常区间待测,比如有没有EndReceive后),如果客户端与本toClient沟通的Socket异常关闭(哪怕数据已经成功存储在服务端,但由于可能该Socket还需继续通信,故客户端异常关闭会导致服务端的toClient某些数据没能正常销毁/关闭/重置,当服务端使用Send之类的函数时就会出现异常。服务端知道是客户端/远程主机强制关闭了连接(因为正常关闭服务端某些属性能记录的)。如果是客户端正常关闭则不属于异常情况(除非服务端执行Send之类的函数,如果是Receive是可以接收部分数据的){即客户端发完数据(数据已经成功存在服务端,可以完全)就可以Close}。

  14、跟13及14点有些关系,当客户端client连接服务端成功后,服务端接受连接生成toClient(或将要生成,如正在serv.EndAccept这一步),如果此时client.Close(),尽管这种关闭方式是双方“协商”后,但是toClient.Connected却仍然是true。也就是说某一方Close,则双方都为“Close”状态,但是只有主动Close的那边的Connected才会重置为false。

  15、这一点很重要:在windows系统中(至少win7是),servSock产生的chatToClient执行BeginReceive(...)进入的回调函数ReceiveCallback是一个在线程池中线程(其它的异步函数的回调函数应该也是),这个有点类似Task;当servSock产生的N个chatToClient并发执行BeginReceive(...)时,由于它们的回调函数ReceiveCallback是属于线程池的,故同时只能执行规定个数的回调函数,因此:并发BeginReceive的情况下,当某个回调函数执行过程中出现阻塞时,它不仅仅自己要暂停,其它的等待线程池中空闲线程回调函数也相当于被阻塞了这句话是错的,因为线程池中并发时是有资源装载和卸载功能,故A线程对应的函数代码执行了一段时间就会“卸载存起来”,然后装载其它没有获得线程执行的“待执行线程”,这个有点类似一个CPU通过时间片的调度方式来“并行”执行电脑上的进程。;但回调函数(不管什么回调函数)里仍然最好不要有数据插入等操作,可开启线程来执行它们。

  16、Socket各种异步函数的回调函数的参数IAsyncResult可以引用多种AsyncResult对象,其中AccpetCallback的是AcceptAsyncResult类(由于它是internal的,别人无法访问);在AcceptAsyncResult对象中有个Result属性,它存的数据能确定每个AcceptCallback得到的能与客户端沟通的Socket:chatToClient的唯一性(Result本质就是chatToClient),故不用担心并发执行到EndAccept(...)会不会造成得到的chatToClient混乱的问题。

  17、当sock.Close()或Dispose()后调用类似sock.Receive()会产生异常ObjectDisposedException:无法访问已释放的对象。sock.ShutDown(....Receive)后如果执行sock.Receive()会产生SocketException:您的主机中的软件中止了一个已建立的连接(如果是对方Close(),我方仍调用Receive之类的函数也会报这个异常(注意,不是释放对象的异常,因为是对方释放,我方只相当于ShutDown一样),但有可能能成功执行一次(比如Send第一次貌似是可以执行的)。Disconnecte(true)会阻塞(具体信息待测);Disconnect(false)执行后还能继续执行接下的BeginReceive并且进入回调函数,回调函数中Connected将会是false,但是EndReceive(iar)不会报错。

  18、不要用sock.Handle.ToInt32()的值作为字典的Key,因为如果你执行了sock.Close()则系统会立刻将sock.Handle.ToInt32()的值回收供下一个Socket对象使用,因此很容易出问题。

  19、sockServ.Listen(N)可以理解为开辟一个普通队列QueListen(只可从一个口一个一个的取数据),它的容量是N,存的是Socket对象,且这些对象就是chatToClient,故哪怕没有Accept只是Listen,客户端也能连接成功,因为客户端连接请求被监听后服务端都会生成一个chatToClient放入QueListen直到达到容量N后就拒绝客户端的连接,并且客户端可以发送数据(因为已经存在chatToClient了)。但是当服务端QueListen满了以后,客户端连接会失败(因为服务端拒绝)并执行回调函数ConnectCallback,但是回调函数里client.Connected是false并且不能EndConnect(iar)(否则抛出目标主机积极拒绝的异常)。当QueListen满了后如果通过Accept/BeginAccept从QueListen里取走最先的chatToClient,则Listen又可以多监听一个连接请求并生成对应的chatToClient到QueListen中(注意,只要Accept后QueListen里就会空出位置而不是说Accept取出的chatToClient还要Close()才会空出位置)。由于QueListen只能一次取一个,故对于代码sockServ.BeginAccept(..);sockServ.BeginAccept(..);是没用的,它只有一个BeginAccept生效。故接收部分不要用异步,用while加Accept加Task就好(Task里接收数据[Receive或BeginReceive看情况],Task里再建立一个Task处理接收的数据)。

  20、TCP连通的情况下,如果中途网络断了,那么服务端/客户端经过一定时间(系统默认的心跳时间?)后能够检测到双方本质上已经没在通信,故会主动关闭连接(DisConnect或ShutDown??不过不会是Close/Dispose),此时再Send等会报:您的主机中的软件中止了一个已建立的连接。

  21、

var iar = sock.BeginConnect("192.168.0.111", 8000, null, null);
// 若没有TCP服务("192.168.0.111", 8000)在监听,则下面会直接返回true,但是Connected是false,故如果此时sock.EndConnect(iar)会报异常。
if(iar.AsyncWaitHandle.WaitOne(3000))
{
//if(sock.Connected)
sock.EndConnect(iar);
else
{
Console.WriteLine("Connected为false。");
}
}

强制转换篇:

  1、将3.5强制转换为整型,则得到的是3;但是假设 short a = -100;将a 强制转换为 ushort类型,则得到不是0,而是-100这个值对应的二进制值直接转换为ushort值,即:65436;反过来也一样,将值为65436的ushort类型变量强制转换为short变量,得到的short变量值是-100而非0或者32668(即将其对应的二进制最高位有1变为0 ) 。

LINQ篇:

  1、若list中的元素是引用类型则 list.Distinct()比较的是list中ele1、ele2、ele3 。。。的引用值而不是引用值指向的对象的值),如果元素是值类型,则比较的是变量(“对象”)的值。这里要注意string类型,比如str1="abc";str2="abc";实际上str1和str2的引用值是一样的,因为系统对字符串会做些特别的记录,如果发现待创建的新的字符串对象的内容已经存在,则不会创建它而是将已存在的字符串对象引用值赋给相应string引用。所以str2="abc"是将str1所创建的"abc"的引用值赋给str2,故str1和str2的引用值及引用值对应的对象值都是一样的。

系统函数篇:

  1、系统自带的GetHashCode()不要用于值类型,因为值类型变量varg只要值相同,则varg.GetHashCode()是一样的;也不要用于字符串,这是因为字符串的特殊性;当用于其它引用类型变量rarg(有对象前提下)时,rarg.GetHashCode()在量不大的情况下是唯一的,但是当引用类型的对象太多时有可能重复(我是以同一类型的Socket类创建了1W个Socket对象,其中有一个重复),但是或许可以这样rarg.GetHashCode().ToString()+rarg.ClassProp1.GetHashCode().ToString()共同构成的Key重复率就大大减少(不过效率会降低)。

关于异常:

  1、异常详细信息后面会带有该异常发生的位置,比如:

详细信息为:{* MySql.Data.MySqlClient.MySqlException (0x80004005): Unable to connect to any of the specified MySQL hosts.
在 MySql.Data.MySqlClient.NativeDriver.Open()
在 MySql.Data.MySqlClient.Driver.Open()
在 MySql.Data.MySqlClient.Driver.Create(MySqlConnectionStringBuilder settings)
在 MySql.Data.MySqlClient.MySqlPool.GetPooledConnection()
在 MySql.Data.MySqlClient.MySqlPool.TryToGetDriver()
在 MySql.Data.MySqlClient.MySqlPool.GetConnection()
在 MySql.Data.MySqlClient.MySqlConnection.Open()
在 Cenbo.Dal.MySqlHelper.Query(String strCmd, String tableName) *}。这里要注意,从上往下是异常发生的范围的扩大(层级更高了),因此对于此信息而言,发生异常的根源是在 MySql.Data.MySqlClient.NativeDriver.Open()

委托篇:

  1、委托 action.BeginInvoke(...)所产生/依赖的线程是在线程池中的,其它类型的委托也一样。

字典Dictionary或ConcurrentDictionary:

  1、最好不要用string类型作为字典的Key,今天就遇到了明明有对应字符串key值,但是TryGetValue却失败的情况(单线程);我猜有可能两次字符串尽管字符串值是相同的,但是引用值不一样导致获取失败。

数组篇:

  1、数组也有0长度的数组(类似List里面没有元素),即 var bytes = new byte[0];是合法的;且bytes.CopyTo(b,...)也不会报错(复制了0字节到b中)。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM