Linux系統程式設計(33)—— socket程式設計之TCP程式的錯誤處理
上一篇的例子不僅功能簡單,而且簡單到幾乎沒有什麼錯誤處理,我們知道,系統呼叫不能保證每次都成功,必須進行出錯處理,這樣一方面可以保證程式邏輯正常,另一方面可以迅速得到故障資訊。
為使錯誤處理的程式碼不影響主程式的可讀性,我們把與socket相關的一些系統函式加上錯誤處理程式碼包裝成新的函式,做成一個模組wrap.c:
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int Accept(int fd, struct sockaddr *sa,socklen_t *salenptr)
{
intn;
again:
if( (n = accept(fd, sa, salenptr)) < 0) {
if((errno == ECONNABORTED) || (errno == EINTR))
gotoagain;
else
perr_exit("accepterror");
}
returnn;
}
void Bind(int fd, const struct sockaddr*sa, socklen_t salen)
{
if(bind(fd, sa, salen) < 0)
perr_exit("bind error");
}
void Connect(int fd, const struct sockaddr*sa, socklen_t salen)
{
if(connect(fd, sa, salen) < 0)
perr_exit("connecterror");
}
void Listen(int fd, int backlog)
{
if(listen(fd, backlog) < 0)
perr_exit("listenerror");
}
int Socket(int family, int type, intprotocol)
{
intn;
if( (n = socket(family, type, protocol)) < 0)
perr_exit("socketerror");
returnn;
}
ssize_t Read(int fd, void *ptr, size_tnbytes)
{
ssize_tn;
again:
if( (n = read(fd, ptr, nbytes)) == -1) {
if(errno == EINTR)
gotoagain;
else
return-1;
}
returnn;
}
ssize_t Write(int fd, const void *ptr,size_t nbytes)
{
ssize_tn;
again:
if( (n = write(fd, ptr, nbytes)) == -1) {
if(errno == EINTR)
gotoagain;
else
return-1;
}
returnn;
}
void Close(int fd)
{
if(close(fd) == -1)
perr_exit("closeerror");
}
慢系統呼叫accept、read和write被訊號中斷時應該重試。connect雖然也會阻塞,但是被訊號中斷時不能立刻重試。對於accept,如果errno是ECONNABORTED,也應該重試。詳細解釋見參考資料。
TCP協議是面向流的,read和write呼叫的返回值往往小於引數指定的位元組數。對於read呼叫,如果接收緩衝區中有20位元組,請求讀100個位元組,就會返回20。對於write呼叫,如果請求寫100個位元組,而傳送緩衝區中只有20個位元組的空閒位置,那麼write會阻塞,直到把100個位元組全部交給傳送緩衝區才返回,但如果socket檔案描述符有O_NONBLOCK標誌,則write不阻塞,直接返回20。為避免這些情況干擾主程式的邏輯,確保讀寫我們所請求的位元組數,我們實現了兩個包裝函式readn和writen,也放在wrap.c中:
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_tnread;
char *ptr;
ptr= vptr;
nleft= n;
while(nleft > 0) {
if( (nread = read(fd, ptr, nleft)) < 0) {
if(errno == EINTR)
nread= 0;
else
return-1;
}else if (nread == 0)
break;
nleft-= nread;
ptr+= nread;
}
returnn - nleft;
}
ssize_t Writen(int fd, const void *vptr,size_t n)
{
size_tnleft;
ssize_tnwritten;
constchar *ptr;
ptr= vptr;
nleft= n;
while(nleft > 0) {
if( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno ==EINTR)
nwritten= 0;
else
return-1;
}
nleft-= nwritten;
ptr+= nwritten;
}
returnn;
}
如果應用層協議的各欄位長度固定,用readn來讀是非常方便的。例如設計一種客戶端上傳檔案的協議,規定前12位元組表示檔名,超過12位元組的檔名截斷,不足12位元組的檔名用'\0'補齊,從第13位元組開始是檔案內容,上傳完所有檔案內容後關閉連線,伺服器可以先呼叫readn讀12個位元組,根據檔名建立檔案,然後在一個迴圈中呼叫read讀檔案內容並存檔,迴圈結束的條件是read返回0。
欄位長度固定的協議往往不夠靈活,難以適應新的變化。比如,以前DOS的檔名是8位元組主檔名加“.”加3位元組副檔名,不超過12位元組,但是現代作業系統的檔名可以長得多,12位元組就不夠用了。那麼制定一個新版本的協議規定檔名欄位為256位元組怎麼樣?這樣又造成很大的浪費,因為大多數檔名都很短,需要用大量的'\0'補齊256位元組,而且新版本的協議和老版本的程式無法相容,如果已經有很多人在用老版本的程式了,會造成遵循新協議的程式與老版本程式的互操作性(Interoperability)問題。如果新版本的協議要新增新的欄位,比如規定前12位元組是檔名,從13到16位元組是檔案型別說明,從第17位元組開始才是檔案內容,同樣會造成和老版本的程式無法相容的問題。
現在重新看看上一節的TFTP協議是如何避免上述問題的:TFTP協議的各欄位是可變長的,以'\0'為分隔符,檔名可以任意長,再看blksize等幾個選項欄位,TFTP協議並沒有規定從第m位元組到第n位元組是blksize的值,而是把選項的描述資訊“blksize”與它的值“512”一起做成一個可變長的欄位,這樣,以後新增新的選項仍然可以和老版本的程式相容(老版本的程式只要忽略不認識的選項就行了)。
因此,常見的應用層協議都是帶有可變長欄位的,欄位之間的分隔符用換行的比用'\0'的更常見,例如本節後面要介紹的HTTP協議。可變長欄位的協議用readn來讀就很不方便了,為此我們實現一個類似於fgets的readline函式,也放在wrap.c中:
static ssize_t my_read(int fd, char *ptr)
{
staticint read_cnt;
staticchar *read_ptr;
staticchar read_buf[100];
if(read_cnt <= 0) {
again:
if( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if(errno == EINTR)
gotoagain;
return-1;
}else if (read_cnt == 0)
return0;
read_ptr= read_buf;
}
read_cnt--;
*ptr= *read_ptr++;
return1;
}
ssize_t Readline(int fd, void *vptr, size_tmaxlen)
{
ssize_tn, rc;
char c, *ptr;
ptr= vptr;
for(n = 1; n < maxlen; n++) {
if( (rc = my_read(fd, &c)) == 1) {
*ptr++= c;
if(c == '\n')
break;
}else if (rc == 0) {
*ptr= 0;
returnn - 1;
}else
return-1;
}
*ptr = 0;
returnn;
}
相關文章
- 二、GO 程式設計模式:錯誤處理Go程式設計設計模式
- Linux Socket C語言網路程式設計:TCP SocketLinuxC語言程式設計TCP
- python網路-Socket之TCP程式設計(26)PythonTCP程式設計
- Linux系統程式設計之訊號中斷處理(下)Linux程式設計
- Linux系統程式設計之訊號中斷處理(上)Linux程式設計
- Linux系統程式設計之程式介紹Linux程式設計
- Linux系統程式設計——特殊程式之孤兒程式Linux程式設計
- socket程式設計在TCP中的應用程式設計TCP
- Linux系統程式設計之匿名管道Linux程式設計
- SOCKET程式設計程式設計
- 網路程式設計之socket程式設計
- 程式設計小技巧之 Linux 文字處理命令(二)程式設計Linux
- linux非阻塞式socket程式設計之select()用法Linux程式設計
- Java 網路程式設計(TCP程式設計 和 UDP程式設計)Java程式設計TCPUDP
- Linux作業系統之Shell程式設計Linux作業系統程式設計
- Linux系統程式設計之檔案IOLinux程式設計
- 玩轉 PHP 網路程式設計全套之 socket stream 程式設計PHP程式設計
- Java Socket程式設計Java程式設計
- socket程式設計(1)程式設計
- Socket程式設計模型程式設計模型
- Python socket程式設計Python程式設計
- PHP回顧之socket程式設計PHP程式設計
- 基於TCP協議的Socket網路程式設計( )TCP協議程式設計
- (3)Tcp Socket程式設計的封裝類 TcpListener/TcpClientTCP程式設計封裝client
- linux網路程式設計中的errno處理Linux程式設計
- 通過 Socket 實現 TCP 程式設計入門TCP程式設計
- JAVA網路程式設計(2)TCP程式設計Java程式設計TCP
- Linux系統程式設計之程式替換:exec 函式族Linux程式設計函式
- Linux系統程式設計之程式間通訊方式:管道(二)Linux程式設計
- Linux系統程式設計之程式間通訊方式:管道(一)Linux程式設計
- 物聯網教程Linux系統程式設計——特殊程式之殭屍程式Linux程式設計
- 物聯網教程Linux系統程式設計——特殊程式之守護程式Linux程式設計
- 【Linux】Linux系統程式設計入門Linux程式設計
- Linux系統程式設計—有名管道Linux程式設計
- Linux系統程式設計基礎Linux程式設計
- Linux系統程式設計入門Linux程式設計
- (Python程式設計 | 系統程式設計 | 並行系統工具 | 程式退出)Python程式設計並行
- golang中的socket程式設計Golang程式設計
- 基於TCP/UDP的Socket程式設計,HTTP/HTTPS協議TCPUDP程式設計HTTP協議