2014年11月3日 星期一

Socket Programming in C# (Server side)





NOTE!!
The source code is deprecated! Please go to my Github: KarateJB/DotNetCore.Socket.Sample, for the latest Socket Server/Client sample code.

注意!!
本文中的程式碼已經過時,請您參考我Github上的最新Socket Server/Client範例程式碼:KarateJB/DotNetCore.Socket.Sample



其實Socket寫法上,不論是ServerClient,都可以使用.NET提供的TCP物件或Socket物件來達到資料傳輸的目的。(因為Socket本身就是走TCP/IP的架構)
本篇文章的目的,在於建立一個Socket Server,讓多個Client可以傳檔案到Server,然後Server上可以顯示連線過的Client數,並在資料傳輸完成後,回傳一個訊息給Client

流程上大致可分為:(WinForm為例)

1    Server

1.1   為了讓Socket Server開始Listening後,仍可顯示連線的Client資訊。 故建立Socket Server後,使用BackgroundWorker來開啟Socket Server
1.2   Socket Server每次接收到新的Client要求時,再開另一條Thread來處理上傳的資料。
1.3   處理完畢後,回傳訊息給Client
1.4   結束Client的連線。
1.5   (重要) 關閉Socket Server時,要先停止Socket Listener (即第一點的BackgroundWorker),才能關閉Socket Server

2    Client

2.1   將上傳檔案名稱及其內容拆成[檔案名稱長度][檔案名稱] [檔案內容],存成Byte Array
2.2   建立TcpClientSocket物件來連線Socket Server
2.3   資料上傳完畢,利用NetworkStream來接收Server端回傳的訊息。
2.4   斷線。

一、 方案參考
1.    UploadSocket.Common : 只有放Log4Net module
2.    UploadSocket.Infra : Model
3.    UploadSocket.Service :
l  封裝Socket的類別 (UploadServer)
l  Client連線後的事件處理 (ClientHandler)
4.    Server and Client as WinForm application

二、 Server side : namespace UploadSocket.Server
1.    Global
//BackgroundWorker識別是否已啟動服務的flag
private bool _isRunning = false;
//Client 識別編號
private int _clientNo = 0;
//Socket Server
private UploadSocket.Service.UploadServer _ftServer = null;
//Server接收檔案的資料夾路徑
private static String UPLOAD_DIR = Application.StartupPath.ToString() + @"\GET\"

private BackgroundWorker _serverBgWorker = new BackgroundWorker();

2.    Socket Server ListenerBackGroundWorker的方式執行,底下是BackGroudWorker DoWorkCode
//指定伺服器背景作業的函式
this._serverBgWorker.DoWork += bgWorker_DoWork;

private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
if (this._isRunning == false){
//Intial Socket Server and open connection
this.initialSocketServer();
//Start listening
this.socketStartListen();
}
}


private void initialSocketServer()
{
this._ftServer = new UploadSocket.Service.UploadServer();
this._ftServer.StartServer(); //啟動Socket Server
this._isRunning = true;
}


private void socketStartListen()
{
while (this._ftServer.IsCloseSocketServerFlg == false)
{
Socket socket4Client = null;
try{
//監聽到來自 socket client 的連線要求
socket4Client = this._ftServer.SocketPrc.Accept();
}
catch (Exception){  return; }

//累加 socket client 識別編號
this._clientNo++;
//顯示於畫面上
String connMsg = String.Format("Client 編號 ({0}) => Server ",
this._clientNo.ToString());
lb_Msg.Items.Add(connMsg);

//用另一條Thread 處理每一個 Socket Client 的要求
using (var handler = new UploadSocket.Service.ClientHandler(
this._clientNo, socket4Client, UPLOAD_DIR))
{
handler.DoCommunicate();
}
}
}


l   以無限迴圈在這一條Thread專做Server接收Client Request
l   在收到一個要求時,馬上丟到另一條Thread去做處理。
l   注意在強制關閉連線(如關閉程式or關掉執行緒)時,會導致Socket Accept方法丟出Exception 這邊採忽略不處理。

3.    Server關閉連線method
private void stopSocketServer()
{
if (_ftServer != null){
this._ftServer.IsCloseSocketServerFlg = true;
this._ftServer.StopServer();
this._ftServer = null;
}
if (this._serverBgWorker.IsBusy){
//取消伺服器背景作業
//this._serverBgWorker.CancelAsync();
this._serverBgWorker.Dispose();
}
this._isRunning = false;
}


l   這邊無法使用CancelAsync()停止listening動作。 可參考BackgroundWorker CancelAsync使用時機

三、 Server side : namespace UploadSocket.Service
接下來是Server端的核心程式
1.    UploadServer
/// <summary>
/// A Upload Server with Socket protocal
/// </summary>
public class UploadServer
{
        /// <summary>
        /// Stop Socket Server flag
        /// </summary>
        public bool IsCloseSocketServerFlg;

        /// <summary>
        /// Socket protocal
        /// </summary>
        public Socket SocketPrc = null;

        //Network endpoint
        private IPEndPoint _ipEnd = null;
        //Max Client connection
        private static int MAX_CLIENT_NUMBER = 100;

        //Constructor
        public UploadServer()
        {
            this.IsCloseSocketServerFlg = false;

            //指定IPEndPoint可接受來自任何port: 5656 的要求
            this._ipEnd = new IPEndPoint(IPAddress.Any, 5656);
            //Socket初始化
            this.SocketPrc = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.IP);
            //ipEnd繫結給Socket
            this.SocketPrc.Bind(_ipEnd);
        }

        //StartServer : 啟動Server
        public void StartServer()
        {
this.SocketPrc.Listen(MAX_CLIENT_NUMBER);
}

//StopServer : 停止Server
public void StopServer()
        {
this.SocketPrc.Close();
this.SocketPrc = null;
this.IsCloseSocketServerFlg = true;
}
}

2.    ClientHandler
public class ClientHandler : IDisposable
{
//Client 識別號碼
        private int _clientNo;
// Socket Client Reuqest
        private Socket _socketConn;
        // 上傳路徑 (不含檔名)
        private String _uploadDir = String.Empty;

        /// <summary>
        /// 建構子
        /// </summary>
        /// <param name="clientNo">Socket Client 識別號碼</param>
        /// <param name="socketConn">Socket Client Reuqest</param>
        /// <param name="uploadDir">Server檔案放置資料夾路徑</param>
        public ClientHandler(int clientNo, Socket socketConn, String uploadDir)
        {
            this._clientNo = clientNo;
            this._socketConn = socketConn;
            this._uploadDir = uploadDir;
        }

public void Dispose()
        {
            if (this._socketConn != null && this._socketConn.Connected)
            {
                //Disconnect connection
                //非必要則由Client自行斷線
//若由Server斷線但Client仍在非同步處理會引發Client side Exception
                //this._socketConn.Shutdown(SocketShutdown.Both);
                //this._socketConn.Close();
                //this._socketConn = null;
            }
        }

        /// <summary>
        /// Server Client 相互通訊
        /// </summary>
        public void DoCommunicate()
        {
            //產生 BackgroundWorker 負責處理每一個 socket client reuqest
            BackgroundWorker bgClient = new BackgroundWorker();
            bgClient.DoWork += new DoWorkEventHandler(bgClient_DoWork);
            bgClient.RunWorkerCompleted += bgClient_DoWorkCompleted;
            bgClient.RunWorkerAsync();
        }
        // 背景作業:處理Client Request
        public void bgClient_DoWork(object sender, DoWorkEventArgs e)
        {
            byte[] clientData = new byte[1024 * 5000];
            int receivedBytesLen = 0;
           
if (this._socketConn.Connected == true)
{
                    //取得socket client連線到ServerStream
                    this.getStreamData(
this._socketConn, ref clientData, out receivedBytesLen);

                    if (receivedBytesLen > 0)
                    {
                        var uploadFile = new UploadFile();
                        this.getUploadFileInfo(
                            clientData, receivedBytesLen, out uploadFile);
                        this.saveUploadFile(
clientData, receivedBytesLen, uploadFile);
                    }

                    //回傳訊息給Client
                    this.rtnServerMsg();
}
        }

        private void rtnServerMsg()
        {
            //正確取得 client requst,再回傳給 client
            string serverResponse =
"Server => clinet(" + _clientNo + ") : 已取得資料";
            byte[] sendData = Encoding.UTF8.GetBytes(serverResponse);
            var netStream = new NetworkStream(this._socketConn);
            netStream.Write(sendData, 0, sendData.Length);
            netStream.Flush();

            Array.Resize(ref sendData, 0);
            sendData = null;
            netStream.Close();
            netStream.Dispose();
        }

        private void saveUploadFile(
byte[] clientData, int receivedBytesLen, UploadFile uploadFile)
        {
            BinaryWriter bWrite = new BinaryWriter(
File.Open(uploadFile.SavedFullPath, FileMode.Create));
            bWrite.Write(
clientData, 4 + uploadFile.FileNameLen,
receivedBytesLen - 4 - uploadFile.FileNameLen);
            bWrite.Close();
        }

        private void getUploadFileInfo(
byte[] clientData, int receivedBytesLen, out UploadFile uploadFile)
        {
            uploadFile = new UploadFile();
            //取得Client上傳的檔案名稱長度
            uploadFile.FileNameLen =
Convert.ToInt16(Encoding.ASCII.GetString(clientData, 0, 4));
            //取得Client上傳的檔案名稱
            uploadFile.FileName =
Encoding.ASCII.GetString(clientData, 4, uploadFile.FileNameLen);
            //取得Client上傳的檔案內容
            uploadFile.FileContent =
Encoding.UTF8.GetString(clientData,
4 + uploadFile.FileNameLen,
receivedBytesLen - 4 - uploadFile.FileNameLen);
            //如果檔案不存在,則先Create file
            uploadFile.SavedFullPath = this.createFile(
Path.Combine(this._uploadDir, uploadFile.FileName));
        }

        private void getStreamData(
Socket mySocket, ref byte[] clientData, out int receivedBytesLen)
        {
            receivedBytesLen = 0;

            #region 使用NetworkStream
            NetworkStream netStream = new NetworkStream(mySocket);
            if (netStream.DataAvailable)
            {
                receivedBytesLen = netStream.Read(clientData, 0, clientData.Length);
            }
            netStream.Close();
            netStream.Dispose();
            #endregion

            #region 使用System.Net.Sockets.Socket Receive方法
            //receivedBytesLen = mySocket.Receive(clientData);
            #endregion

        }

        private String createFile(String fileFullPath)
        {

            if (!File.Exists(fileFullPath))
            {
                File.Create(fileFullPath).Close(); //產生LOG
            }
            return fileFullPath;
        }
    }

l   getStreamData方法中有兩種方式可以取得Socket Stream data
l   Socket Stream data編碼方式為:
0 ~ 3 Bytes = 檔名長度;
4 ~ 4 + 檔名長度 = 檔名長度;
後面其餘Bytes為檔案資料。


四、 Infra : namespace UploadSocket.Infra
Server side用到的Model …
public class UploadFile
    {
        /// <summary>
        /// 檔案名稱長度
        /// </summary>
        public Int16 FileNameLen { get; set; }
        /// <summary>
        /// 檔案名稱
        /// </summary>
        public String FileName { get; set; }
        /// <summary>
        /// 檔案內容
        /// </summary>
        public String FileContent { get; set; }
        /// <summary>
        /// Server存放的完整檔案路徑
        /// </summary>
        public String SavedFullPath { get; set; }
    }

五、 Server side : Snapshot




沒有留言:

張貼留言