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