最近浏览了几篇有关Socket发送消息的文章,发现大家对Socket Send方法理解有所偏差,现将自己在开发过程中对Socket的领悟写出来,以供大家参考。
(一)架构
基于TCP协议的Socket通信,架构类似于B/S架构,一个Socket通信服务器,多个Socket通信客户端。Socket通信服务器启动时,会建立一个侦听Socket,侦听Socket将侦听到的Socket连接传给接受Socket,然后由接受Socket完成接受、发送消息,当Socket存在异常时,断开连接。在实际开发项目中,往往要求Socket通信服务器能提供高效、稳定的服务,一般会用到以下技术:双工通信、完成端口、SAEA、池、多线程、异步等。特别是池,用的比较多,池一般包括一下几种:
1)Buffer池,用于集中管控Socket缓冲区,防止内存碎片。
2)SAEA池,用于集中管控Socket,重复利用Socket。
3)SQL池,用于分离网络服务层与数据访问层(SQL的执行效率远远低于网络层执行效率)。
4)线程池,用于从线程池中调用空闲线程执行业务逻辑,进一步提高网络层运行效率。
(二)Send
主服务器接受Socket为一端口,客户端Socket为一端口,这两个端口通过TCP协议建立连接,通信基础系统负责管理此连接,它有两个功能:
1)发送消息
2)接受消息
Socket的Send方法,并非大家想象中的从一个端口发送消息到另一个端口,它仅仅是拷贝数据到基础系统的发送缓冲区,然后由基础系统将发送缓冲区的数据到连接的另一端口。值得一说的是,这里的拷贝数据与异步发送消息的拷贝是不一样的,同步发送的拷贝,是直接拷贝数据到基础系统缓冲区,拷贝完成后返回,在拷贝的过程中,执行线程会IO等待, 此种拷贝与Socket自带的Buffer空间无关,但异步发送消息的拷贝,是将Socket自带的Buffer空间内的所有数据,拷贝到基础系统发送缓冲区,并立即返回,执行线程无需IO等待,所以异步发送在发送前必须执行SetBuffer方法,拷贝完成后,会触发你自定义回调函数ProcessSend,在ProcessSend方法中,调用SetBuffer方法,重新初始化Buffer空间。
口说无凭,下面给个例子:
服务器端:
客户端:
解释:
客户端第一次发送数据:1234567890。
客户端第一个接受数据:1234567890,该数据由服务端用Send同步方法发送返回。
客户端第二个接受数据:1234567890,该数据由服务端用Send异步方法发送返回。
以上似乎没什么异常,好,接下来,我只发送abc。
客户端第一个接受数据:abc,理所当然,没什么问题。
客户端第二个接受数据:abc4567890!为什么呢?应该是abc才对呀!
好,现在为大家解释一下:
异步发送是将其Buffer空间中所有数据拷贝到基础系统发送缓冲区,第一次拷贝1234567890到发送缓冲区,所以收到1234567890,第二次拷贝abc到发送缓冲区,替换了先前的123,所以收到abc4567890,大家明白的?
源码:
BufferManager
using System.Collections.Generic;
using System.Net.Sockets;// This class creates a single large buffer which can be divided up
// and assigned to SocketAsyncEventArgs objects for use with each // socket I/O operation. // This enables bufffers to be easily reused and guards against // fragmenting heap memory.// // The operations exposed on the BufferManager class are not thread safe.class BufferManager{ int m_numBytes; // the total number of bytes controlled by the buffer poolbyte[] m_buffer; // the underlying byte array maintained by the Buffer Manager Stack<int> m_freeIndexPool; //int m_currentIndex; int m_bufferSize;public BufferManager(int totalBytes, int bufferSize)
{ m_numBytes = totalBytes; m_currentIndex =0; m_bufferSize = bufferSize; m_freeIndexPool =new Stack<int>(); }// Allocates buffer space used by the buffer pool
publicvoid InitBuffer() { // create one big large buffer and divide that // out to each SocketAsyncEventArg object m_buffer =newbyte[m_numBytes]; }// Assigns a buffer from the buffer pool to the
// specified SocketAsyncEventArgs object // // <returns>true if the buffer was successfully set, else false</returns>publicbool SetBuffer(SocketAsyncEventArgs args) {if (m_freeIndexPool.Count >0)
{ args.SetBuffer(m_buffer, m_freeIndexPool.Pop(), m_bufferSize); } else { if ((m_numBytes - m_bufferSize) < m_currentIndex) { returnfalse; } args.SetBuffer(m_buffer, m_currentIndex, m_bufferSize); m_currentIndex += m_bufferSize; } returntrue; }// Removes the buffer from a SocketAsyncEventArg object.
// This frees the buffer back to the buffer poolpublicvoid FreeBuffer(SocketAsyncEventArgs args) { m_freeIndexPool.Push(args.Offset); args.SetBuffer(null, 0, 0); }}
SocketAsyncEventArgsPool
using System;
using System.Collections.Generic;using System.Net.Sockets;// Represents a collection of reusable SocketAsyncEventArgs objects.
class SocketAsyncEventArgsPool{ Stack<SocketAsyncEventArgs> m_pool;// Initializes the object pool to the specified size
// // The "capacity" parameter is the maximum number of // SocketAsyncEventArgs objects the pool can holdpublic SocketAsyncEventArgsPool(int capacity) { m_pool =new Stack<SocketAsyncEventArgs>(capacity); }// Add a SocketAsyncEventArg instance to the pool
// //The "item" parameter is the SocketAsyncEventArgs instance // to add to the poolpublicvoid Push(SocketAsyncEventArgs item) { if (item ==null) { thrownew ArgumentNullException("Items added to a SocketAsyncEventArgsPool cannot be null"); } lock (m_pool) { m_pool.Push(item); } }// Removes a SocketAsyncEventArgs instance from the pool
// and returns the object removed from the poolpublic SocketAsyncEventArgs Pop() { lock (m_pool) { return m_pool.Pop(); } }// The number of SocketAsyncEventArgs instances in the pool
publicint Count { get { return m_pool.Count; } }}
Server
using System;
using System.Threading;using System.Net.Sockets;using System.Net;using System.Text;// Implements the connection logic for the socket server.
// After accepting a connection, all data read from the client // is sent back to the client. The read and echo back to the client pattern // is continued until the client disconnects.class Server{ privateint m_numConnections; // the maximum number of connections the sample is designed to handle simultaneously privateint m_receiveBufferSize;// buffer size to use for each socket I/O operation BufferManager m_bufferManager; // represents a large reusable set of buffers for all socket operationsconstint opsToPreAlloc =2; // read, write (don't alloc buffer space for accepts) Socket listenSocket; // the socket used to listen for incoming connection requests // pool of reusable SocketAsyncEventArgs objects for write, read and accept socket operations SocketAsyncEventArgsPool m_readWritePool; int m_totalBytesRead; // counter of the total # bytes received by the serverint m_numConnectedSockets; // the total number of clients connected to the server Semaphore m_maxNumberAcceptedClients;// Create an uninitialized server instance.
// To start the server listening for connection requests // call the Init method followed by Start method // // <param name="numConnections">the maximum number of connections the sample is designed to handle simultaneously</param> // <param name="receiveBufferSize">buffer size to use for each socket I/O operation</param>public Server(int numConnections, int receiveBufferSize) { m_totalBytesRead =0; m_numConnectedSockets =0; m_numConnections = numConnections; m_receiveBufferSize = receiveBufferSize; // allocate buffers such that the maximum number of sockets can have one outstanding read and //write posted to the socket simultaneously m_bufferManager =new BufferManager(receiveBufferSize * numConnections * opsToPreAlloc, receiveBufferSize);m_readWritePool =new SocketAsyncEventArgsPool(numConnections);
m_maxNumberAcceptedClients =new Semaphore(numConnections, numConnections); }// Initializes the server by preallocating reusable buffers and
// context objects. These objects do not need to be preallocated // or reused, but it is done this way to illustrate how the API can // easily be used to create reusable objects to increase server performance. //publicvoid Init() { // Allocates one large byte buffer which all I/O operations use a piece of. This gaurds // against memory fragmentation m_bufferManager.InitBuffer();// preallocate pool of SocketAsyncEventArgs objects
SocketAsyncEventArgs readWriteEventArg;for (int i =0; i < m_numConnections; i++)
{ //Pre-allocate a set of reusable SocketAsyncEventArgs readWriteEventArg =new SocketAsyncEventArgs(); readWriteEventArg.Completed +=new EventHandler<SocketAsyncEventArgs>(IO_Completed); readWriteEventArg.UserToken =new AsyncUserToken(); // assign a byte buffer from the buffer pool to the SocketAsyncEventArg object m_bufferManager.SetBuffer(readWriteEventArg);// add SocketAsyncEventArg to the pool
m_readWritePool.Push(readWriteEventArg); }}
// Starts the server such that it is listening for
// incoming connection requests. // // <param name="localEndPoint">The endpoint which the server will listening // for connection requests on</param>publicvoid Start(IPEndPoint localEndPoint) { // create the socket which listens for incoming connections listenSocket =new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); listenSocket.Bind(localEndPoint); // start the server with a listen backlog of 100 connections listenSocket.Listen(100);// post accepts on the listening socket
StartAccept(null);//Console.WriteLine("{0} connected sockets with one outstanding receive posted to each....press any key", m_outstandingReadCount);
Console.WriteLine("Press any key to terminate the server process...."); Console.ReadKey(); } // Begins an operation to accept a connection request from the client // // <param name="acceptEventArg">The context object to use when issuing // the accept operation on the server's listening socket</param>publicvoid StartAccept(SocketAsyncEventArgs acceptEventArg) { if (acceptEventArg ==null) { acceptEventArg =new SocketAsyncEventArgs(); acceptEventArg.Completed +=new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed); } else { // socket must be cleared since the context object is being reused acceptEventArg.AcceptSocket =null; }m_maxNumberAcceptedClients.WaitOne();
bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg); if (!willRaiseEvent) { ProcessAccept(acceptEventArg); } }// This method is the callback method associated with Socket.AcceptAsync
// operations and is invoked when an accept operation is complete //void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e) { ProcessAccept(e); }privatevoid ProcessAccept(SocketAsyncEventArgs e)
{ Interlocked.Increment(ref m_numConnectedSockets); Console.WriteLine("Client connection accepted. There are {0} clients connected to the server", m_numConnectedSockets);// Get the socket for the accepted client connection and put it into the
//ReadEventArg object user token SocketAsyncEventArgs readEventArgs = m_readWritePool.Pop(); ((AsyncUserToken)readEventArgs.UserToken).Socket = e.AcceptSocket;// As soon as the client is connected, post a receive to the connection
bool willRaiseEvent = e.AcceptSocket.ReceiveAsync(readEventArgs); if (!willRaiseEvent) { ProcessReceive(readEventArgs); }// Accept the next connection request
StartAccept(e); }// This method is called whenever a receive or send operation is completed on a socket
// // <param name="e">SocketAsyncEventArg associated with the completed receive operation</param>void IO_Completed(object sender, SocketAsyncEventArgs e) { // determine which type of operation just completed and call the associated handlerswitch (e.LastOperation) { case SocketAsyncOperation.Receive: ProcessReceive(e); break; case SocketAsyncOperation.Send: ProcessSend(e); break; default: thrownew ArgumentException("The last operation completed on the socket was not a receive or send"); }}
// This method is invoked when an asynchronous receive operation completes.
// If the remote host closed the connection, then the socket is closed. // If data was received then the data is echoed back to the client. //privatevoid ProcessReceive(SocketAsyncEventArgs e) { // check if the remote host closed the connection AsyncUserToken token = (AsyncUserToken)e.UserToken; if (e.BytesTransferred >0&& e.SocketError == SocketError.Success) { //increment the count of the total bytes receive by the server Interlocked.Add(ref m_totalBytesRead, e.BytesTransferred); Console.WriteLine("The server has read a total of {0} bytes", m_totalBytesRead); Int32 BytesToProcess = e.BytesTransferred; Byte[] bt =new Byte[BytesToProcess]; Buffer.BlockCopy(e.Buffer, e.Offset, bt, 0, BytesToProcess); string strReceive = Encoding.Default.GetString(bt); Send(token.Socket, bt, 0, bt.Length, 1000); Thread.Sleep(1000);//echo the data received back to the client
//e.SetBuffer(e.Offset, e.BytesTransferred);bool willRaiseEvent = token.Socket.SendAsync(e); if (!willRaiseEvent) { ProcessSend(e); }}
else { CloseClientSocket(e); } } publicstaticvoid Send(Socket socket, byte[] buffer, int offset, int size, int timeout) { socket.SendTimeout =0; int startTickCount = Environment.TickCount; int sent =0; // how many bytes is already sentdo { if (Environment.TickCount > startTickCount + timeout) //throw new Exception("Timeout.");try { sent += socket.Send(buffer, offset + sent, size - sent, SocketFlags.None); } catch (SocketException ex) { if (ex.SocketErrorCode == SocketError.WouldBlock || ex.SocketErrorCode == SocketError.IOPending || ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable) { // socket buffer is probably full, wait and try again Thread.Sleep(30); } else throw ex; // any serious error occurr } } while (sent < size); }
// This method is invoked when an asynchronous send operation completes.
// The method issues another receive on the socket to read any additional // data sent from the client // // <param name="e"></param>privatevoid ProcessSend(SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { //e.SetBuffer(e.Offset, 10);// done echoing data back to the client
AsyncUserToken token = (AsyncUserToken)e.UserToken; // read the next block of data send from the clientbool willRaiseEvent = token.Socket.ReceiveAsync(e); if (!willRaiseEvent) { ProcessReceive(e); } } else { CloseClientSocket(e); } }privatevoid CloseClientSocket(SocketAsyncEventArgs e)
{ AsyncUserToken token = e.UserToken as AsyncUserToken;// close the socket associated with the client
try { token.Socket.Shutdown(SocketShutdown.Send); } // throws if client process has already closedcatch (Exception) { } token.Socket.Close();// decrement the counter keeping track of the total number of clients connected to the server
Interlocked.Decrement(ref m_numConnectedSockets); m_maxNumberAcceptedClients.Release(); Console.WriteLine("A client has been disconnected from the server. There are {0} clients connected to the server", m_numConnectedSockets);// Free the SocketAsyncEventArg so they can be reused by another client
m_readWritePool.Push(e); }}
Program
using System;
using System.Net;using System.Collections.Generic;using System.IO;class Program
{ staticvoid Main(string[] args) { IPEndPoint iep =new IPEndPoint(IPAddress.Parse("10.1.20.6"), 1333);Server objServer =new Server(1000, 10);
objServer.Init(); objServer.Start(iep); }}