[ 永遠的UNIX::UNIX技術資料的寶庫 ]   GB | BIG5

首頁 > 編程技術 > 網絡編程 > 正文
TCP 客戶-服務器例子
本文出自:http://sunsland.top263.net 作者: (2001-10-23 07:00:00)

概述

回射服務器:echo server 

7th 端口; /etc/inetd.conf 

1.客戶從標準輸入讀一行文本,寫到服務器上

2.服務器從網絡輸入讀此行,並回射給客戶

3.客戶讀此回射行並寫到標準輸出。


幾個讀寫有關的函數

fputs 和 fgets 標準庫函數

//簡單的輸入和輸出程序:

// mystr_client.c
#include "unp.h"
void main(void)
{
char sendline[MAXLINE],recevline[MAXLINE];
fgets(sendline, MAXLINE, stdin); //標準輸入
memcpy(recevline, sendline, strlen(sendline));
fputs(recevline, stdout); //標準輸出
}

//writen 和 readline字節流讀寫函數

ssize_t Writen(int filedes, const void *buff, size_t nbytes);
ssize_t Readline(int filedes, void *buff, size_t maxlen);


TCP回射服務器程序

//main源程序:

#include "unp.h"

int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;

listenfd = Socket(AF_INET, SOCK_STREAM, 0);//創建套接口

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);//9877

Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//綁定端口

Listen(listenfd, LISTENQ);

for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);//等待客戶完成連接accept

if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}


//服務器程序str_echo 函數

//讀如一行 Readline

//回射此行 Writen

#include "unp.h"
#include "sum.h"

void
str_echo(int sockfd)
{
ssize_t n;
struct args args;
struct result result;

for ( ; ; ) {
if ( (n = Readn(sockfd, &args, sizeof(args))) == 0)
return; /* connection closed by other end */

result.sum = args.arg1 + args.arg2;
Writen(sockfd, &result, sizeof(result));
}
}


TCP回射客戶端程序

//main創建套接口,裝填IP地址結構socket bzero與服務器連接connect

#include "unp.h"

int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;

if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

str_cli(stdin, sockfd); /* do it all */

exit(0);
}

//tcp回射客戶程序str_cli函數

#include "unp.h"

void
str_echo(int sockfd)
{
long arg1, arg2;
ssize_t n;
char line[MAXLINE];

for ( ; ; ) {
if ( (n = Readline(sockfd, line, MAXLINE)) == 0)
//從服務器讀回射行

return; /* connection closed by other end */

if (sscanf(line, "%ld%ld", &arg1, &arg2) == 2)
snprintf(line, sizeof(line), "%ld\n", arg1 + arg2);
else
snprintf(line, sizeof(line), "input error\n");

n = strlen(line);
Writen(sockfd, line, n);//寫到服務器
}
}


正常啟動


服務器台啟動 ./tcpserv01 &

檢查監聽套接口狀態 netstat a

過濾:netstat a |grep 9877

啟動客戶端程序 ./tcpcli01 127.0.0.1

檢查TCP的連接 netstat a

過濾:netstat a |grep 9877

檢查進程的狀態和關系 ps l

tty控制台 ; pts/0偽終端

pid進程編號 ppid 父進程編號

父進程的ppid 是shell (bash)

狀態S 

I 空閑 Idle S 停止 stop Z 僵屍 zombie

狀態 WCHAN

鍵入 EOF (Control-D)結束


TCP連接終止序列

客戶端鍵入EOF字符

fgets返回一個空指針

str_cli返回main 

main通過調用exit終止

客戶套接口由內核關閉

客戶TCP發送一個FIN給服務器

服務器TCP則以ACK響應

此時,服務器套接口處CLOSEWAIT狀態,客戶套接口處FINWAIT2狀態

服務器TCP接收FIN時,服務器子進程阻塞readline調用.readline返回0

函數str_echo返回服務器子進程的main

服務器子進程通過調用exit來終止

服務器子進程中打開的所有描述字隨之關閉。

由進程來關閉已連接套接口引發TCP連接終止序列的最兩個分節:

一個從服務器到客戶的FIN和一個從客戶到服務器的ACK。至此,連接完全終止,

客戶套接口進入TIMEWAIT狀態。

進程終止處理的另一部分是在服務器子進程終止時給父進程發一個信號SIGCHLD。

但我們在代碼中未捕獲,是出現了:僵屍進程 zombie process,用

ps -la 查看


Posix 信號處理

信號signal:某事件發生時對進程的通知(軟中斷)

信號是異步的不可提前知道信號發生的時間

信號的流向:進程->進程(本身)    內核->進程

SIGCHLD: 內核在某進程終止時發給父進程的信號

每個信號都有處理方法disposition /action


處理函數sigaction的選擇

信號處理程序(signal handler)來捕獲(catching)信號.

void handler(int signo);SIGKILL , SIGSTOP不能被捕獲


設置信號的處理辦法為SIGIGN來忽略信號;SIGKILL , SIGSTOP不能被捕獲

設置信號的處理辦法為SIG_DFL來設置缺省處理辦法,  在接收到信號時終止進程

 特定信號還在當前工作目錄產生一個進程的核心映像。

個別信號的缺省處理辦法是忽略,SIGCHLD就是缺省處理辦法是忽略的信號。

signal 函數

通用的信號處理函數:sigaction,signal

為了方便起見,建立新的信號處理函數signal

Sigfunc *signal(int signo,Sigfunc *func);


/lib/ signal.c /desktop/signal.txt


一旦安裝了信號處理程序,它便一直安裝著。

當一個信號處理程序正在執行時,所遞交的信號是阻塞的除了被捕獲的信號外,

沒有額外信號阻塞。如果一個信號在阻塞時生成了一次或多次,在信號解阻塞

一般只遞送一次。缺省時Unix信號是不排隊的有選擇性地阻塞或不阻塞一組信號

是可能的可以在某段臨界區代碼執行時,不許捕獲某些信號, 以此來保護這段代碼。


處理SIGCHLD 信號

設置僵屍(zombie)狀態的目的:維護子進程的信息,以便父進程在稍的某個時候取回。

僵屍(zombie))狀態的信息:子進程的ID 終止狀態

子進程的資源利用信息(CPU時間、內存等等)。

如果一個進程終止,且該進程有子進程處僵屍狀態,則所有僵屍子進程的父進程ID均

置為1(init進程)。

init進程將作為這些子進程的繼父並負責清除它們(init進程將wait它們,從而去除僵屍

進程)。僵屍進程輸出的COMMAND列為<defunt>(ps命令輸出)。

僵屍進程的危害:佔用內核空間,最終導致無法工作。

處理僵屍進程

處理SIGCHLD的Signal 函數在listen之, accept之:

void Signal (SIGCHID, sig_chld)
{
pid+t pid;
int stat;
pid= wait(&stat);
prtinf(“child %d terminated\n”, pid);
return;
}

處理步驟

鍵入EOF字符來終止客戶,客戶TCP發一個FIN給服務器,服務器以ACK響應。

FIN的接收導致服務器TCP遞送一個EOF給子進程阻塞中的readline,從而子進程終止。

當信號SIGCHLD遞交時,父進程阻塞accept調用。函數sig_chld(信號處理程序)執

行,它調用函數wait取到子進程的PID和終止狀態,再調用printf.然返回。

由該信號是在父進程阻塞慢系統調用(accept)時由父進程舖獲的,所以內核將

使accept返回一個EINTE錯誤(被中斷的系統調用).而父進程不處理此錯誤,所以中

止。//有些系統。


wait 和waitpid 函數

pid_t wait (int *statloc);
pid_t waitpid(pid_t pid,int *statloc, int options);

二者均返回:進程ID為0成功,或-1出錯

如果沒有終止的子進程讓進程來調用wait,但有一個或多個正在執行的子進程,則阻塞

直到第一個現有子進程終止。

waitpid對等待哪個進程及是否阻塞給了我們更多的控制:參數pid讓指定想等待的進程ID,

值-1表示等待第一個終止的子進程。參數option指定附加選項。最常用的選項是wNOHANG,

它通知內核在沒有己終止子進程時不要阻塞。

調用wait並不足以防止出現僵屍進程:

5個信導都在信號處理程序執行之前產生,信號處理程序又只執行一次,因為unix 信號

一般是不排對的。更嚴重的是,此問題是不確定的,信號處理程序可能執行三次或四次。

在我們剛剛運行的例子中,客戶與 服務器在同一主機上,信號處理程序執行一次,留下

四個僵屍進程。但若我們在不同的主機 上運行客戶和服務器,信號處理程序一般執行兩

次:一次作為第一個產生的信號的結果,由 另外4個信號在信號處理程序執行時發生,

所以處理程序一般情況下去再被調用一次,這 就留下了三個僵屍進程。但有時,可能依

賴FIN到達服務器主機的時機,信號處理程序執 行三次或四次。

正確的解決辦法是調用watpid而不是wait.

在循環內調用waitpid取得了所有已終止子進程的狀態。

指定選項WNOHANG,它告訴waitpid在有末終止的子進程運行 時不要阻塞。

不能在循環中調用wait,因為沒有辦法防止wait在有未終止的子進程運行時阻塞。


關數據格式的問題

客戶和服務器之間傳遞文本

在本地轉換和處理文本,不論客戶和服務器主機的字節序如何,客戶和服務器都能工

作得很好。

在客戶與服務器之間傳遞二進制結構潛在的問題:

不同的實現以不同的格式存儲二進制數,最常用的方法便是大端格式與小端格式

不同的實現在存儲相同的C數據類型時可能不同。32bits/64bits?

不同的實現給結構打包的方式也是不同的,取決所用數據類型的垃數及機器的對

齊限制,跨套接口來傳送二進制結構是很不明智的。


解決此數據格式問題常用方法

把所有的數值數據作為文本串來傳遞要以兩個主機有相同的字符集為基礎。

顯式定義所支持數據類型的二進制格式位效,大端或小端,在客戶與服務器之間以此

格式傳遞所有數據。


(http://www.fanqiang.com)
    進入【UNIX論壇

相關文章
 

★  樊強制作 歡迎分享  ★