1. 程式人生 > >socket程式設計—技術實現

socket程式設計—技術實現

這幾天都在玩socket了,有一點心得,貼出來與大家共賞,若有不妥或錯誤的地方,還請各位看官指點一二。

什麼是socket?socket就是...,我在這裡就不抄書了,有興趣的同仁去查查書吧。
不過還要說一句,socket就是不同程序之間的一種通訊方式。就象打電話是朋友之間的一種通訊方式是一樣。個人理解:所謂“通訊”,就是相互之間傳送資料。有人理解socket是不同計算機之間的一種通訊方
式,這是不確切的。兩個程序,不管是執行在同一臺計算機上,還是執行在不同計算機上,都可通過
socket技術進行通訊。

socket套接字的使用需要有網絡卡的支援,所以socket一般都被用來在不同機器之間通訊,而如果在同一臺計算機上的兩個程序進行通訊,通常採用效率更高的共享記憶體技術來實現。

兩個程序之間進行通訊,就需要兩個程序同時都在運行了(廢話),在具體實現中,兩個程序我們通常要區別對待,一個程序專門等待另一個程序給自己發訊息,收到訊息後進行處理,在把處理結果傳送回去。我們把專門處理訊息、提供服務的程序稱為伺服器端,把傳送訊息、請求處理的程序稱為客戶端。總體過程就是客戶端傳送一個訊息給伺服器端,伺服器端程序收到訊息進行處理,把處理結果傳送給客戶端。恩,就是這樣。

還有一個問題,如果我現在有一個程序要跟另一臺計算機上的某個程序進行socket通訊,那在我這個程序中如何指定另一個程序呢?這裡還需要說一下另一個概念——埠,如果把作業系統比作一座房子的話,那埠就是房子的視窗,是系統外界同系統內部進行通訊的通道。在socket實現中,我們不進行另一個程序的指定,而是指定傳送訊息或接收訊息的埠號。比如說現在程序A要給程序B發訊息,我們會把訊息傳送到程序B所執行的計算機的埠N上,而程序B此時正在監視埠N,這樣程序B就能收到程序A傳送來的資料,同樣程序B也把訊息傳送到該埠上,程序A也能從該埠收到程序B傳送來的資料,當然,這需要客戶端和伺服器端關於埠號進行一個約定,即共同操作同一個埠。如果客戶端把訊息傳送到埠N1上,而伺服器端監視的是埠N2,那通訊一定不能成功。埠號最大為65535,不能比這個再大了,但在我們自己的程式中儘量不要用小於1024的埠號,小於1024的埠好很多都被系統使用了,比如23被telnet所使用。

socket的實現是很簡單的,只要按照一定的步驟,就可馬上建立一個這樣的通訊通道。

下面較詳細的介紹幾個核心的函式:

SOCKET socket(int af, int type, int protocol);
無論是客戶端還是伺服器端,下面這個函式是一定要用到的,也是最先用到的。
這個函式是要告訴系統,給我準備好一個socket通道,我要和其它程序通訊了。函式的返回值很重要,我們要記下來,它表示系統為我們準備好的這個socket通道,在以後的每個socket相關函式中都會用到,如果這個值等於SOCKET_ERROR,表示函式執行失敗了。函式的引數我們分別給:PF_INET、SOCK_STREAM和IPPROTO_TCP。

int bind(SOCKET s, const sockaddr *addr, int namelen);
這個函式只有伺服器端程式使用,作用是與某個socket通道繫結。可以用返回值判斷該函式執行結果怎麼樣,如果等於SOCKET_ERROR,那就是失敗了。第一個引數s,就是socket()函式的返回值;在結構addr中,我們要給定一個埠號;namelen等於結構sockaddr的大小。

int listen(SOCKET s, int backlog);
這個函式只有伺服器端程式使用,作用是監聽該埠。返回值與bind函式意義一樣。

int accept(SOCKET s, sockaddr *addr, int *addrlen);
這個函式只有伺服器端程式使用,作用是響應客戶端的連線。返回值與bind函式意義一樣。

int connect(SOCKET s, const sockaddr *name, int namelen);
這個函式只有客戶端程式使用,作用是把客戶端和某個計算機的某個埠建立連線。返回值與bind函式意義一樣。第一個引數s,就是socket()函式的返回值;在結構name中,我們要給定一個埠號和目的機器名;namelen等於結構sockaddr的大小。

int send(SOCKET s, char *buf, int len, int flags);
int recv(SOCKET s, char *buf, int len, int flags);
這兩個函式就是傳送資料和接收資料,客戶端和伺服器端程式都能用,哪個傳送哪個接收不用說了吧?呵呵。
從函式的返回值可以檢查函式執行是否成功。引數中buf是指向傳送或接收的資料的指標,len是資料長度。flags我們給個0就可以(其實是我不知道具體含義)。

最後就是關閉socket了,這個很容易忘掉,但這個函式很重要,一定要用。
int closesocket(SOCKET s);


好了,關鍵函式就這麼幾個,下圖是這幾個函式的執行順序:

client端 service端

  |     |
  v     v
socket() socket()
  |     |
  |     v
  |   bind()
  |     |
  |     v
  |   listen()
  |     |
  |     v
  |   accept() 掛起,直到有客戶端來連線
  |     |
  v   三段握手過程   |
connect() <-------------> |
  |     |
  v   傳送訊息   v
  +---> send() ---------------> recv() <-------+
  |   |     . |
  |   |     . 處理訊息 |
  |   v   響應訊息   . |
  +---- recv() <--------------- send() --------+
  |     |
  v     |
close() ---------------> recv()
    |
    v
  closesocket()

上圖我覺得能很好的說明客戶端和伺服器端的執行軌跡。

使用以上幾個函式在 linux 系統上就可成功建立一個socket通訊連路,但如果在windows系統上,還要用到另一個函式:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
在windows系統上,首先要執行這個函式,所以要把這個函式放在socket()函式的前面。

我對上面的函式進行了一些封裝,為節省篇幅,我去掉所有註釋和非重要的函式,在這裡可以看到各個函式的具體用法:

在 VC60 環境下要執行下面的函式,要包含標頭檔案 errno.h 和 winsock2.h,還有,在連線的時候要連線上ws2_32.dll檔案。

這是標頭檔案內容:
class Socket {
public:

bool setup();

void close();

bool connect(string host, int port);

bool listen();

int accept();

int recv(char *buf, int len);

int recv(int new_fd, char *buf, int len);

int send(const char *msg, int len);

int send(int new_fd, const char *msg, int len);

private:
  int _fd;
};

這是實現檔案內容:
bool Socket::setup() {

WSADATA wsd;
_fd = WSAStartup(MAKEWORD(2,2), &wsd);
if(_fd) {
return false;
}

_fd = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_fd == -1) {
return false;
}
return true;
}

bool Socket::listen() {
struct sockaddr_in my_addr;

my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(52309);
my_addr.sin_addr.s_addr = INADDR_ANY;

if(::bind(_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == SOCKET_ERROR) {
return false;
}

if(::listen(_fd, BACKLOG) == SOCKET_ERROR) {
return false;
}

return true;
}

int Socket::accept()
{
int new_fd;
struct sockaddr_in their_addr;
int sin_size = sizeof(their_addr);

printf("accepting... \n");

new_fd = ::accept(_fd,
  (struct sockaddr *)&their_addr,
  &sin_size);
return new_fd == SOCKET_ERROR ? -1:new_fd;
}

bool Socket::connect(string host, int port) {
struct hostent *_h = gethostbyname(host.c_str());
if (_h == 0) {
return false;
}

struct in_addr *_addr = (struct in_addr *)_h->h_addr;
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr = *_addr;
sin.sin_port = htons(port);

if (::connect(_fd, (sockaddr *)&sin, sizeof(sin)) == SOCKET_ERROR) {
return false;
}

return true;
}

int Socket::recv(int new_fd, char *buf, int len)
{
int nb = ::recv(new_fd, buf, len, 0);
if (nb == -1) {
printf("Error! recv.\n");
}
return nb;
}

int Socket::recv(char *buf, int len) {
return recv(_fd, buf, len);
}

int Socket::send(const char *msg, int len) {
return send(_fd, msg, len);
}

int Socket::send(int new_fd, const char *msg, int len)
{
int nb = ::send(new_fd, msg, len, 0);
if (nb == -1) {
printf("Error! send.\n");
}

return nb;
}

void Socket::close() {

int trytimes = 0;
while(::closesocket(_fd) && trytimes < CLOSE_TRY_TIMES)
trytimes++;

if(trytimes == 10) {
printf("Cannot close socket!\n");
}
}

好,socket類是封裝好了,下面就是組織了,伺服器端和客戶端是不一樣的,下面分別給出程式碼,到這裡已經就很簡單了。

客戶端:
int main(int argc, char **argv)
{
printf("socket of client is run ...\n");
Socket s;
if (!s.connect("dezhi", 52309))
return 0;

char *msg = "ok, send a message.";
for (int i=0; i<10; i++) {
s.send(msg, 20);
printf("message = %s\n", msg);
}
s.send("q", 1);
s.close();

return 0;
}

伺服器:
int main(int argc, char **argv) {
printf("socket of service is run ...\n");

Socket s;
s.listen();
int new_fd = s.accept();

char buf[8];
buf[7] = '\0';
while (1) {
if (s.recv(new_fd, buf, 5) != -1) {
  printf("%s\n", buf);
  if (buf[0] == 'q')
  break;
}
}
s.close();
}

下面為執行結果:
客戶端:
socket of client is run ...
Socket: WSAStartup success execute.
Socket: socket success execute.
Socket: Establish the connection to "127.0.0.1:52309"
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
message = ok, send a message.
Socket: Close connection to "127.0.0.1:52309"
Press any key to continue

伺服器端
socket of service is run ...
Socket: WSAStartup success execute.
Socket: socket success execute.
bind ok!
listen ok!
accepting...
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
ok, send a message.
qk, send a message.
Press any key to continue

就到這裡吧。socket的相關內容可遠不止這些,我在這裡只是給大家來個拋磚引玉,想深究?路還很漫長。關於詳細的實現程式碼,去我的《原始碼》上找吧,不放在這裡,是為了讓篇幅小些。