Socket Programming

2002.4.29,   Ultimagic GameStudio

註:這篇文章是我從以前大學報告中節錄出來的,而且為了配合網頁而再次作修改,內容包括Winsock及Java的Socket Programming。而在這之前因為學過一段時間的Java Programming,而對Java的Socket Programming有很深的印象,Java不愧是網路霸主,而且還是跨平台的程式語言,再加上易學易用的Thread,使Java強到令我汗顏。Java的Socket真的非常的Easy,但是Java有一項致命的缺陷,那就是「速度」……Java是出了名的龜速。

1. Socket簡介

    在Socket介面還沒有定出來之前,很多網路程式都還要去控制TCP/IP的Protocol,是一件非常浩大的工程。而因為難度很高,所以一般人就算有很好、很創新的idea,卻都只有望而卻步,不能真正發揮到程式設計師的創意。這就是為什麼Socket會出現在這樣的時代。而Socket介面並不是Windows獨有的,最早有Socket出現的地方是在BSD,我們很明顯的知道BSD等大型作業系統不但要處理多人多工,還要求能遠端控制,所以Socket會在這樣的環境下開發出來並不另人感到意外。而WinSock的全名是Windows Socket,是定義於Windows TCP/IP application Client與TCP/IP的protocol stack之間的一項標準介面。而雖說「標準」其實也和事實有些出入,因為在很多情況下Winsock和大型主機作業系統不同,除了30個socket function來自其老祖宗BSD以外,其他的大多是Windows自行定義的。不過這也難以避免,因為Windows的需求對象特殊,而且為了符合這個作業系統的環境,所以Winsock堂然就和一般的socket不太一樣。不過學習Winsock時所用到的函式,大多還是相容於其他作業系統的,而且學習Winsock時也可以更進一步的了解網路的各種運作以及其規格。

    這是個網路極為發達的年,生在這樣的年代,要比別人早一步得到資訊,或是更便捷的找到自己要的資料,「網路」確實是一大利器。而在這樣廣大的需求下,好的網路軟體就顯得很重要。大家都知道看網頁要用Netscape或IE,上BBS要用telnet,上FTP要用ftp,而要離線瀏覽,則可以選擇Teleport,若要網路傳呼還有ICQ、CICQ等工具,網路工具可以說是應有盡有。那麼學Winsock還能寫什麼樣的程式呢?這就要大家發揮創意了。而目前台灣遊戲界也慢慢的朝向網路發展了,眾所皆知的免費的「宏基戲谷」以及「DiabloII」、或是以B2C收費為主要導向的「天堂」、「金庸online」、「英雄」、「紅月」、「龍族」等,便是用網路來進行遊戲,不但在遊戲進行中可以和對方抬槓、丟水球等,還可以進行線上的遊戲教學。更可以在遊戲中加入Banner的廣告設計來增加收入,是很好的idea。要是這樣的技術再延伸,還可以在遠端控制電腦,如控制監視系統攝影機。如果在足夠的頻寬下,想要只用攝影機就能做Video conference也不是件難事。類似這樣的應用還可以再推廣、想像,因為Winsock己經減輕了程式設計師的負擔了,所以我們可以大憺的去想像我們未來的通訊方式,而用Winsock去實現它。

2. Winsock解析

    要使用Winsock,我們就必須了解Winsock的基本通訊原理。右圖就是說明Winsock API到硬體之間的運作方式。而在這個圖中,我們可以很明顯地看到Winsock應用程式是屬於最上層的介面,程式之下便是Windows Socket的重態連結檔(Dynamic Link Library),這樣的關係,在Windows下是非常常見的。而在往下走就是Windows設定的通訊協定,而實現這種通訊協定的硬體就在正下方,接著就是網卡的RJ-45等線路和Router等連上廣域網路的硬體實體。

    右圖便能了解Winsock和硬體之間的關係,然而這就是不夠的,因為Winsock的軟體設計方式才是最別出心裁的地方。廢話不多說了,下面就讓大家看看這個別出心裁的設計吧。

    首先,我們必須知道在Socket的通訊中,靠的不是別的,正是「Socket」這個抽象的東西。一個Socket在程式的觀點就只是一個系統分配給你方數字罷了,就好像Windows Handler一樣,但這個數字卻決定了網路的傳輸和運作。這就是Socket介面的目的之一:不再讓程式設計師看到可怕的package,而且抽象的Socket來想像網路資料傳送的。用一個很簡單的例子來說明:

    當我們向系統要到一個socket時,先要告訴系統這個socket是要用來listen還是用來connect。listen的一方就是所謂的Server,而connect的一方則是所謂的Client。而我們listen的這個socket的port如果在21,而對如果知道IP,並且用port為21的socket要求做connect時,就可以馬上得到對方的要求,而做進一步的連線或斷線。

    這個簡單的例子主要在說明socket的運作機制,這個機制不但很合理,而且也方便了所有網路的通訊。

    運作機制雖然很簡單,不過以上有說到幾個keyword要特別說明的。首先,是所謂的listen,當一個socket是listen時,這個socket本身只有接受和拒絕的能力,並不是真的用來傳輸的socket。而當這個socket接受了Client的連線請求,系統就會配置一個新的socket來做資料傳輸用,而原先listen的socket則繼續等待別的Client的連線。所以Server在平常沒有Client上線時,就會多用一個socket,就是因為這個原因。如果各位使用過Serv-U這個軟體,會發現在沒有人上線的情況下,程式還是會占著一個socket,這個socket就是server用來listen的socket。大家都知道Server是服務Client的,而並不是所有的Client都會被接受而服務,這就是因為Server可以選擇要服務的對,而不是盲目的服務,要不然可就天下大亂了。

    而Client則是要求服務的,所以當Client取得connect的socket時,除了可以要求連接外,其所有的通訊,也都一概仰賴這個socket來傳輸,所以可想而知Client的設計較為簡單。

    另外我們還要考慮的因素還有很多,這些考量都只會出現在Winsock的initialization時,而這些設定會影響雙方的傳輸,所以這裡更要仔細的介紹了。

    首先,是決定傳輸的方式,Winsock的傳輸方式可分為二種:Stream Socket和Datagram Socket。Stream Socket是TCP式傳,提供雙向、有順序、可信賴、不重覆的資料串流;而Datagram Socket是UDP式傳輸,也提供雙向,但不保證可以有順序、可信賴、不重覆的資料片段。然而我們的程式為了方便和更高的可靠性,我們並不使用Datagram而建議使用Sream,以保資料正確性。

    再來就是之前所提到的port的問題,port的觀念本來是為了減輕單一主機的load而定的。而這個觀念也被帶到socket的觀念中,每一個socket都有一個port,而只有在port相同的情況下才能達成連線。在這裡就列舉幾個定義在winsock.h中的標準port:

/*
 * Port/socket numbers: network standard functions
 */
#define IPPORT_ECHO       7
#define IPPORT_DISCARD    9
#define IPPORT_SYSTAT     11
#define IPPORT_DAYTIME    13
#define IPPORT_NETSTAT    15
#define IPPORT_FTP        21
#define IPPORT_TELNET     23
#define IPPORT_SMTP       25
#define IPPORT_TIMESERVER 37
#define IPPORT_NAMESERVER 42
#define IPPORT_WHOIS      43
#define IPPORT_MTP        57

    相信大家應該對這些標準port不陌生才是,像是ftp server port=21, smtp server port=25, Name server port=42,然而並不是一定要照這些標準port來設,也不是占著21port的server就非當ftp server不可。只是標準是標準,要架黑站也未嘗不可。不過要是沒有特定功能的Server,最好避免使用這些標準port。因為在同一個IP下,不允許有兩個Server程式共用一個port,也就是說,一個port不能有兩個程式來listen。

    另外在server程式的Initialization階段,我們會看到一個奇怪的函式叫bind(),這個函式就是在listen前先把IP和port的資訊綁在這個socket上,使socket有運作listen的能力,這個函式會在分析程式時有更明確的說明。

    從上面的介紹,應該已知道winsock程式的兩大部分,即Server和Client。而這裡當然要分兩個部分來介紹,尤其是Initialization的部分,更是要好好的說明一下了。首先就先看看Server是如何初始化的吧。

(1) Server部分

// 變數宣告
BOOL bStartup=FALSE;
short ServerIPPort=5;
WSADATA WSAData;
SOCKET ServerSocket=INVALID_SOCKET;
SOCKADDR_IN Server_sin;
// 啟動Winsock.dll動態連結程式庫
if(WSAStartup(MAKEWORD(1,1),&WSAData))
{
    MessageBox("Can't use winsock.dll","error");
    SendMessage(WM_DESTROY,0,0);
}
// 設置這個Flag,以區別Winsock是否已啟動
else bStartup=TRUE;
// 向系統取要求取得Socket
if((ServerSocket=socket(AF_INET,SOCK_STREAM,0))==SOCKET_ERROR)
{
    MessageBox("Can't create socket!","error");
    SendMessage(WM_DESTROY,0,0);
}
// 準備好要bind的資料後,即把資料綁在Socket上
Server_sin.sin_family=AF_INET;
Server_sin.sin_port=htons(ServerIPPort);
Server_sin.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(ServerSocket,(LPSOCKADDR)&Server_sin,sizeof(Server_sin))<0)
{
    MessageBox("Can't bind Server Addr!","error");
    SendMessage(WM_DESTROY,0,0);
}
// 開始等待Client連線
if(listen(ServerSocket,SOMAXCONN)<0)
{
    MessageBox("Can't listen!","error");
    SendMessage(WM_DESTROY,0,0);
}
// 這是配合Message Driven架構,只要有Client被接受,即發出Message
WSAAsyncSelect(ServerSocket,m_hWnd,WM_WSAEVENT,FD_ACCEPT);


    以上這一小段程式就是在Server主程式中,在視窗建立時也做了Winsock的初始化。一開始出現的這個WSAStartup()是要呼叫Winsock.dll,因為所有的Winsock API都是靠這個動態連結檔來完成的。接著我們看到一個變數叫bStartup,這是代表Boolean值的Startup flag,用來指示Winsock.dll是否有被喚起,以利程式結束後,再釋放(Release)Winsock,要釋放的函式是WSACleanup()。這些都只是較無關緊要的函式,要知詳情請查閱MSDN,這裡就不多做介紹了。接著就介紹其他重要的函式吧:

    ★ socket() ─ 這個函式有三個引數,第一個引數AF_INET是指這個socket是用在internet的通訊,也就是說其Address Family是屬於internet型態的。而第二個引數SOCK_STREAM則是通訊方式,在之前已經有提到過,STREAM可以提供有順序、可靠度高、不重覆的資料串流。而第三個引數是protocol,而0表示IP的一般protocol。

    ★ htons() ─ 這個函式,它是用來轉換host IP port數字的函數,因為我們指定的數字只有我們看得懂,網路並不知道那是什麼東西,利用這個函式就可以幫我們把數字轉成網路系統能辨識的值。其傳回值是short型態。

    ★ htonl() ─ 同上個函式,但是其傳回值是long型態。

    在上面小段程式中,我們也可以看到有一個變數是宣告為SOCKADDR_IN的型態,這就是Internet型態的位址表示資料結構,當然他的原型也被宣告在winsock.h中,以下就來看看他的原型宣告(prototype):

/*
 * Socket address, internet style
 */
struct sockaddr_in {
    short sin_family;
    u_short sin_port;
    struct in_addr sin_addr;
    char sin_zero[8];
};

    這個Structure最後的sin_zero[8],不是浪費空間哦!而是為了讓所有的socket address符合最原始宣告的SOCKADDR結構,所以沒有用到的多餘空間就用sin_zero[8]來填滿了。在這裡我們同時也看到了sin_family,和前面一樣,這個值就用AF_INET就可以了。而sin_port則可以隨便給,反正是自己設定的Server嘛。而sin_addr就有學問了,這個可不是限制Client IP用的,而止限制Server IP,也就是說,要是我在這裡設定sin_addr=inet_addr("140.118.233.42"),則這個Server就只能在IP為140.118.233.42的電腦上跑,所以我們的程式並沒有這樣設定,而是設定為sin_addr=htonl(INADDR_ANY);這樣設定就可以在各種不同的IP的機器上正常工作了。完成了這個Structure的設定之後,接著就是一個非常重要的工作了,那就是把剛剛設定好的這個Structure資訊綁在我們之前得到的Socket上面,使他具有聆聽(listen)的能力。而用bind()就是做這種事情,其第一個引數就是我們的Socket,第二個就是我們剛設定好的SOCKADDR_IN的sTRUCTURE位址,而第三個引數則是這個Structure的大小,可以很容易用sizeof()來取得。當這些資訊綁到socket上面時,這個socket並不會開始發生作用,所以當我們要它工作時,就要使用listen()這個函式了。在Win32 console(MS-Dos模式 under Win95/98)下使用listen()會使程式靜止聆聽Client連線,而因為在Win32 Architecture下的視窗化應用程式是Message Driven,平常沒有動作時,程式也不會進入靜止狀態,因為程式要是進入了這個狀態就無法更新應用程式的視,以到於別的程式移開我們的程式後,會造成無法更新被遮蔽的部分而形同當機。所以在視窗化應用程式使用getch()等函式都會被視為無效的,而listen()也是一樣的,而它的第一個引數必須是綁上資訊而未有連線的socket,第二個引數為最大的等待連線Queue長度(the Length of pending connection),這裡我們通常使用SOMAXCONN這個Predefined value。

    到這裡為止,socket()bind()listen()都是標準的socket介面函式,接下來我們看到的這個WSA(Windows Socket Asynchronous)開頭的函式,全都是Winsock延伸定義出來的,也是為了符合Windows視窗環境所定義的。剛剛提到listen()完後,其實就開始了Server的運作,但是listen()並不能使程式靜止等待,那就算有Client發出連線請求,我們的程式也無從得知,所以接著就要使用Winsock延伸定義的「非同步式socket」了,說是「非同步」其實就是說「message driven」,在Windows視窗化程式中,每個程式都有自己的Message Queue,而這個Message Queue就是用來駕駛每個視窗的重要角色。我們可以看到WSAAsyncSelect()就是把socket狀態用message的方式傳到程式的Message Queue,以便程式做進一步的動作。WSAAsyncSelect()的第一個引數還是我們的socket,而第二個引數就是我們的主視窗程式的handle,第三個引數就是我們自己定義的視窗訊息,上面兩個引數都是Windows programming的東西,在這裡不多做解釋,卻知詳請可以MSDN或查閱各種Windows programming的書。第四個引數則是Winsock定義的Socket Event,因為這個Socket只有接受請求的能力,而沒有通訊的能力,所以我們只要去判斷FD_ACCEPT事件就可以了。

    經過這一連串的初始化,Server就真的可以開始運作了。首先我們還要看一下,Server是如何接受Client的連線請求:

// Message處理片段
afx_msg long OnWSAEvent(WPARAM wParam,LPARAM lParam)
{
    switch(lParam)
    {
        case FD_ACCEPT:
            SOCKET tempSocket;
            SOCKADDR_IN tempSockAddr;
            int iLength;
            iLength=sizeof(SOCKADDR_IN);
            if((tempSocket=accept(ServerSocket,(LPSOCKADDR)&tempSockAddr,&iLength))==SOCKET_ERROR)
            {
                MessageBox("Not Accept","error");
            }
            WSAAsyncSelect(tempSocket,m_hWnd,WM_WSAUSEREVENT,FD_ACCEPT|FD_READ|FD_WRITE|FD_CLOSE);
            UserNum++;
            break;
    }
    return 0;
}

    以上這一段程式就是用來接受(Accept)客戶端的請求連,而我們所看到的tempSocket就是連線後可以真正用來通訊的socket,那大家可能又有疑問了,為什麼這麼重要的socket,我卻用暫時性的變數?這個就和上面所說的WSAAsyncSelect()有關係了,而且也和windows message driven有很大的牽連。為了解釋這個,就必須先說說windows message driven的架構了。首先,當一個message放入message queue時也會放入wParam和lParam這兩個參數,而我們在這裡不去特別留住這個可以通訊的socket就是因為當有進一步的message觸發時,我們只要去讀取這個message的wParam或是lParam就可以得這個可以通訊的socket。也因為這樣,所以我們為了讓事件觸發message所以我們在後面又用了一次WSAAsyncSelect()這個函式。而這一次是用在可以通訊的socket上,所以我們要注意的事件有FD_ACCEPT、FD_READ、FD_WRITE、FD_CLOSE等。其中FD_ACCEPT我們已看過一次了,而FD_READ表示對方有資料到了,而FD_WRITE表示自己有資料要給對方,而FD_CLOSE顧名思義就是斷線了。

    再這裡要特別注意的是,當Message出現時,其傳來的wParam參數代表的是Socket,而lParam則是事件的動作,可能是FD_ACCEPT、FD_READ、FD_WRITE或FD_CLOSE。

afx_msg long OnWSAUserEvent(WPARAM wParam,LPARAM lParam)
{
    switch(lParam)
    {
        case FD_CONNECT:
            break;
        case FD_READ:
            // 讀取從Client來的資料
            break;
        case FD_WRITE:
            // 傳送到Client的資料
            break;
        case FD_CLOSE:
            closesocket(wParam);
            break;
    }
    return 0;
}

    以上是Server處理message的程式片段。

 

(2) Client部分

// 向系統取得socket
if((ClientSocket=socket(AF_INET,SOCK_STREAM,0))==SOCKET_ERROR)
{
    MessageBox("Can't create socket!","error");
    DestroyWindow();
    return false;
}
// 先做好要連線到server的address和port的設定,就可以用connect()來連線了
Remote_sin.sin_family=AF_INET;
Remote_sin.sin_addr.s_addr=inet_addr(lpServerIP);
Remote_sin.sin_port=htons(IPPORT_ECHO);
if(connect(ClientSocket,(LPSOCKADDR)&Remote_sin,sizeof(Remote_sin))==SOCKET_ERROR)
{
    MessageBox("Can't establish connection!","error");
    DestroyWindow();
    return false;
}
WSAAsyncSelect(ClientSocket,m_hWnd,WM_WSAEVENT,FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE);

    以上就是Client程式中的初始化片段程式,經過上面的介紹後,相信聰明的讀者可以很快的意會這些程式碼的意義。首先,還是得向系統取得連線用的socket,再來只要填一下要連線的Server Address和Server Port就可以用connect()函式來要求Server做連線,接著也是用WSAAsyncSelect()來定義要被處理的message,而在Client中被處理的是FD_CONNECT、FD_READ、FD_WRITE和FD_CLOSE。

afx_msg void OnWSAEvent(WPARAM wParam,LPARAM lParam)
{
    switch(lParam)
    {
    case FD_CONNECT:
        // 這裡要處理的是:當Server接受連線,要傳給Server什麼訊息...
        break;
    case FD_READ:
        // 這裡要處理接受Server來的訊息。
        break;
    case FD_WRITE:
        // 這裡要處理傳送到Server的訊息。
        break;
    case FD_CLOSE:
        closesocket(ClientSocket);
        ClientSocket=INVALID_SOCKET;
        bLogin=FALSE;
        MessageBox("close socket");
        break;
    }
}

    在前面都提得差不多了,而且就如前面提到過的,Client是向Server提出服務要求的,所以這個程式就簡單多了。因為這個程式只要管使用者要什麼服務,而程式再經由我們自己定義的溝通方式和Server要求服務,這樣一來,Client/Server的架構就完全出來了。在後面所附的原始程式中,我們儲發現Server的程式比Client大了四到五倍,這應該不足為奇,因為Server不只是管理多人的程式,更是服務多人的程式,所以才會集中火力討論Server。如果讀者有與趣,可以到作品集>程式精選下載TServer和TClient程式和原始碼來參考,而以下則列舉它們訊息傳送的代表意義:

Client → Server (Request) Command String

Register:<UserName>,<Password>,
<NickName>,<Sex>,<Birthday>,
<Age>,<EMail>;
當使用者使用NewUser的登錄資訊要求新的帳號,Client端會集合所有使用者資訊,然後作成這一長串的字串送給Server,接著Client端會收到回應,這個回應如果是「Completed」字串,則登錄成功,否則會傳回一段錯誤訊息。
Login:<UserName>,<Password>; 當Client端發出這個指令來告訴Server,已啟用的使用者帳號要求進入本Server。這個指令成功或失敗Server都不會有回應,因為連不上可能是使用者要求的Server IP有錯,所以萬一連不上去,就一定是三個錯誤其中之一,所以Server並不回應這個指令。事實上,Server IP填錯的話,Server也無從回應起。
Logout:<UserName>; 這個就不必多說了,當使用者按下Logout的按鈕,Client程式就會送這個指令給Server告訴它本使用者已登。這個指令送出後,Server會以另一種格式的指令傳給其他線上使用者,關於這類的Command String請參考下面的「Server→Client Command String」。
其實我們要結束通訊只要其中一方使用closesocket()就可以了,但是這樣的話,被停掉的另外一方並不會知道這個socket已經斷了,所以這個指令的存在是有必要的,也是為了Server方便管理線上的使用者而設計的。
SendMessage:<UserName>,
<DestUserName>,<MessageString>
這就是一對一的使用者傳訊指令,而以第一個參數<UserName>是自己的名字,而第二個參數<DestUserName>是對方的名字,第三個參數<MessageString>當然就是所要說的話。而大家可能會注意到,前面那些指令後面都有一個分號(;)以表示結,而這個指令是沒有分號結尾的,因為若是使用者傳的訊息中本來就有分號,這樣就會被誤判為結束字元,所以在這裡沒有分號結尾,而以一般C語言的NULL character結尾,這樣就沒有問題了。
這個訊息Server會以另一種格式的指令傳給<DestUserName>使用者。關於這類的Command String請參考下面的「Server→Client Command String」。
ClientExecute:<UserName>,
<DestUserName>,<FullPathName>
這個功能就是執行對方的程式或檔案,以達到「遠端控制」的功能。第一個<UserName>還是自己的名字,第二個參數<DestUserName>是對方的名字,而第三個參數<FullPathName>是對方磁碟上的檔案完整路徑檔名。
這個訊息Server會以另一種格式的指令傳給<DestUserName>使用者。關於這類的Command String請參考下面的「Server→Client Command String」。
SendGlobalMessage:<UserName>,
<MessageString>
這個指令其實就是BroadCast,就是把訊息給所有上線的使用者看到。這個指令的第一個參數<UserName>是自己的名字,第二個參數就是要傳出去的訊息,因為是BroadCast所以就不須要有<DestUserName>了。這個功能可以用在聊天室的製作。
這個訊息Server會以另一種格式的指令傳給<DestUserName>使用者。關於這類的Command String請參考下面的「Server→Client Command String」。

    以上就是所有的「Client→Server Command String」,因為這個東西完全是自己定義的,大家都可以有自己的格式,甚至可以做到先行編碼後再做傳輸,這稍後會再討論。接著,就介紹一下「Server→Client Command String」。

Server→Client (Response) Command String

AddOnline:<UserName>; 當有一個Client使用者連上Server時,別人的Client視窗就會顯示出這位剛上線的使用者。這就是Server用這個指令告訴其他線上使用者的。
DeleteOnline:<UserName>; 當有一個Client斷線時,Server也會用這個指令告訴其他的使用者,好讓Client程式把這位使用者從使用者列表中移除。
Message:<FromUserName>,<MessageString> 當Client收到這個訊息,表示有其他的使用者傳這個訊息過來,這是一對一的傳訊。
Execute:<FromUserName>,<FullPathName> 當Client收到這個訊息,表示有其他使用者希望執行這個使用者的程式或檔案,或是開啟網址等…
GlobalMessage:<FromUserName>,<MessageString> 當Client收到這個訊息,表示有其他的使用者或自己發出BroadCast的訊息,所以Client程式會把這個訊息和發信人顯示在聊天室的框架內。

(3) 訊息傳輸格式解析

    這裡我要介紹的是傳輸時的格式,為什麼要介紹這個東西呢?因為Socket在傳輸資料的時候,可能會一次來多筆的資料,而這些資料如果沒有用一個好的機制和格式來幫助讀取,可能會整個亂掉。例如:當訊息A傳來時,系統正在處理訊息A時,訊息B和訊息C又到了,而在buffer中等待讀取,這時,如果你設計的程式沒有辦法正確劃分訊息B和訊息C,結果可能導致資料錯誤或Lost其中一個訊息。

    在Socket Programming中,我們可以把我們要傳送的一筆訊息視為「Package」,而這個Package的格式,在一般網路上都是這樣運作的:

    如上圖即為一筆Package,而這筆資料中,會分為兩個部分:

Length - 使用2bytes,記錄Package總長度。

Transmit Data - 不限長度,記錄所要傳輸的資料。

    利用這種格式的Package,就算很多筆資料堆積在Buffer中,也不會亂掉了。因為只要在讀取下一筆資料時,先讀取2個bytes的長度資訊,就可以知道這一筆資料需要用多大的容量一次讀入。所以,每個package長度可以是不固定的,而且還不會造成讀取上的困難,是一般網路上常用的方式。

    而其中的Transmit Data,更可以加以規劃及編碼,以提高傳輸之安全性,不然只要被破解,Server可能會遭到「惡搞」的命運。關於編碼機制,將於Encryption文件中加以討論之。

 

 

2. Java Socket Programming

    Java上的Socket Programming相當的簡單,而且程式短小精幹,所以就用程式碼來介紹吧。以下是一個在console下Single Server& Multi-Client程式。

(1) Server Side:

import java.lang.*;
import java.io.*;
import java.net.*;    // 使用到Socket就要引入

// 當有一個使用者上線就開一Thread來處理。
class CommThread extends Thread
{
    private Socket s;
    // Constructor
    public CommThread(Socket s)
    {
        this.s=s;
    }
    // 主要通訊機制
    public void Communicate() throws IOException
    {
        BufferedReader in=new BufferedReader(new InputStreamReader(s.getInputStream()));
        while(true)
        {
            if(in.ready())
            {
                String str=in.readLine();
                System.out.print(s.getInetAddress().getHostName());
                System.out.print("-->");
                System.out.println(str);
                if(str.equals("quit"))
                {
                    s.close();
                    return;
                }
            }
        }
    }
    // Thread一定要有這個方法,主要在呼叫Communication()及處理IOException
    public void run()
    {
        try{
                Communicate();
        }
        catch(IOException e)
        {
            System.out.println(e.getMessage());
        }
    }
}
// 主類別
class LinServer
{
    // 宣告用來聆聽的ServerSocket
    private ServerSocket ss;
    public LinServer(int port) throws IOException
    {
        ss=new ServerSocket(port);
    }
    public Socket ServerListen() throws IOException
    {
        return ss.accept();
    }
    public void ServerShutdown() throws IOException
    {
        ss.close();
    }
    // 程式進入點
    public static void main(String[] args) throws IOException
    {
        System.out.println("Server Start...");
        // 產生Instance並設定ServerPort=5
        LinServer server=new LinServer(5);
        // 當使用者連上時,開一Thread後,繼續聆聽
        while(true)
        {
            Socket s=server.ServerListen();
            CommThread t1=new CommThread(s);
            t1.start();
        }
    }
}

(2) Client Side:

import java.lang.*;
import java.io.*;
import java.net.*;

class LinClient
{
    private Socket s;
    public LinClient(String host,int port) throws IOException
    {
        s=new Socket(host,port);
    }
    public void Communication() throws IOException
    {
        BufferedReader keyin=new BufferedReader(
            new InputStreamReader(System.in));
        BufferedWriter out=new BufferedWriter(
            new OutputStreamWriter(s.getOutputStream()));
        while(true)
        {
            System.out.print("Input String:");
            String str=keyin.readLine();
            out.write(str,0,str.length());
            out.newLine();
            out.flush();    // 下完這個指令,才產生資料傳輸動作
            if(str.equals("quit"))
            {
                return;
            }
        }
    }
    public void Logout() throws IOException
    {
        s.close();
    }
    public static void main(String[] args) throws IOException
    {
        BufferedReader keyin=new BufferedReader(
            new InputStreamReader(System.in));
        System.out.print("Input Server Name:");
        String sername=keyin.readLine();
        System.out.print("Input Server Port:");
        int port=Integer.valueOf(keyin.readLine()).intValue();
        LinClient client=new LinClient(sername,port);
        client.Communication();
        client.Logout();
    }

}