1. 程式人生 > >Linux下的socket程式設計實踐(四)TCP服務端優化和常見函式

Linux下的socket程式設計實踐(四)TCP服務端優化和常見函式

併發下的殭屍程序處理

只有一個程序連線的時候,我們可以使用以下兩種方法處理殭屍程序:

1)通過忽略SIGCHLD訊號,避免殭屍程序

    在server端程式碼中新增

    signal(SIGCHLD, SIG_IGN);

2)通過wait/waitpid方法,解決殭屍程序

signal(SIGCHLD,onSignalCatch);  
  
void onSignalCatch(int signalNumber)  
{  
    wait(NULL);  
}  
那麼如果是多程序狀態下多個客戶端同時關閉呢?


我們可以用下面的客戶端程式碼測試:

/** client端實現的測試程式碼**/  
int main()  
{  
    int sockfd[50];  
    for (int i = 0; i < 50; ++i)  
    {  
        if ((sockfd[i] = socket(AF_INET, SOCK_STREAM, 0)) == -1)  
            err_exit("socket error");  
  
        struct sockaddr_in serverAddr;  
        serverAddr.sin_family = AF_INET;  
        serverAddr.sin_port = htons(8001);  
        serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  
        if (connect(sockfd[i], (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)  
            err_exit("connect error");  
    }  
    sleep(20);  
}  
此時由於訊號的同時到達,並且SIGCHLD又是不可靠訊號,不支援排隊,會留下相當部分的殭屍程序


解決方法:

使用迴圈的  waitpid函式就可以將所有的子程序留下的殭屍程序處理掉

void sigHandler(int signo)  
{  
    while (waitpid(-1, NULL, WNOHANG) > 0)  
        ;  
} 
//pid=-1 等待任何子程序,相當於 wait()。
//WNOHANG 若pid指定的子程序沒有結束,則waitpid()函式返回0,不予以等待。若結束,則返回該子程序的ID。
地址查詢的API
#include <sys/socket.h>  
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //獲取本地addr結構  
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //獲取對方addr結構  
  
int gethostname(char *name, size_t len);  
int sethostname(const char *name, size_t len);  
  
#include <netdb.h>  
extern int h_errno;  
struct hostent *gethostbyname(const char *name);  
  
#include <sys/socket.h>       /* for AF_INET */  
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);  
struct hostent *gethostent(void);  
//hostent結構體  
struct hostent  
{  
    char  *h_name;            /* official name of host */  
    char **h_aliases;         /* alias list */  
    int    h_addrtype;        /* host address type */  
    int    h_length;          /* length of address */  
    char **h_addr_list;       /* list of addresses */  
}  
#define h_addr h_addr_list[0] /* for backward compatibility */ 

這兩個函式呼叫的時機很重要,否則不能得到正確的地址和埠:

TCP

     對於伺服器來說,在bind以後就可以呼叫getsockname來獲取本地地址和埠,雖然這沒有什麼太多的意義。getpeername只有在連結建立以後才呼叫,否則不能正確獲得對方地址和埠,所以他的引數描述字一般是連結描述字而非監聽套介面描述字。

對於客戶端來說,在呼叫socket時候核心還不會分配IP和埠,此時呼叫getsockname不會獲得正確的埠和地址(當然連結沒建立更不可能呼叫getpeername),當然如果呼叫了bind 以後可以使用getsockname。想要正確的到對方地址(一般客戶端不需要這個功能),則必須在連結建立以後,同樣連結建立以後,此時客戶端地址和埠就已經被指定,此時是呼叫getsockname的時機。

UDP

    UDP分為連結和沒有連結2種(這個到UDP與connect可以找到相關內容)

沒有連結的UDP不能呼叫getpeername,但是可以呼叫getsockname,和TCP一樣,他的地址和埠不是在呼叫socket就指定了,而是在第一次呼叫sendto函式以後

已經連結的UDP,在呼叫connect以後,這2個函式都是可以用的(同樣,getpeername也沒太大意義。如果你不知道對方的地址和埠,不可能會呼叫connect)。

獲取本機所有IP:
/**獲取本機IP列表**/  
int gethostip(char *ip)  
{  
    struct hostent *hp = gethostent();  
    if (hp == NULL)  
        return -1;  
  
    strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr));  
    return 0;  
}  
  
int main()  
{  
    char host[128] = {0};  
    if (gethostname(host, sizeof(host)) == -1)  
        err_exit("gethostname error");  
  
    cout << "host-name: " << host << endl;  
    struct hostent *hp = gethostbyname(host);  
    if (hp == NULL)  
        err_exit("gethostbyname error");  
  
    cout << "ip list: " << endl;  
    for (int i = 0; hp->h_addr_list[i] != NULL; ++i)  
    {  
        cout << '\t'  
             << inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]) << endl;  
    }  
  
    char ip[33] = {0};  
    gethostip(ip);  
    cout << "local-ip: " << ip << endl;  
}  
簡述TCP 11種狀態



1.客戶端和伺服器連線建立的時候,雙方處於ESTABLISHED(建立)狀態


2.關於TIME_WAIT狀態  詳見  http://www.mamicode.com/info-detail-190400.html

3.TCP/IP協議的第11種狀態:圖上只包含10種狀態,還有一種CLOSING狀態

產生CLOSING狀態的原因:

  Server端與Client端同時關閉(同時呼叫close,此時兩端同時給對端傳送FIN包),將產生closing狀態,最後雙方都進入TIME_WAIT狀態。(因為主動關閉的一方會進入TIME_WAIT狀態,雙方同時主動關閉,則都進入)

SIGPIPE訊號

往一個已經接收FIN的套接中寫是允許的,接收到FIN僅僅代表對方不再發送資料;但是在收到RST段之後,如果還繼續寫,呼叫write就會產生SIGPIPE訊號,對於這個訊號的處理我們通常忽略即可。

signal(SIGPIPE, SIG_IGN); 

  SIGPIPE,雖然已經接受到FIN,但是我還可以傳送資料給對方;如果對方已經不存在了,那麼TCP會進行重置,TCP協議棧傳送RST段,收到RST後,再進行write會產生SIGPIPE訊號。

  其實很好理解:TCP可以看作是一個全雙工的管道,讀端訊號不存在了,如果再對管道進行寫的話會導致SIGIPPE訊號的產生,處理的時候是忽略這個訊號就可以了,其實就是按照管道的規則。

  我們測試的時候Client傳送每條資訊都發送兩次,Server端關閉之後Server端會發送一個FIN分節給Client第一次訊息傳送之後, Server端會發送一個RST分節給Client端;第二次訊息傳送(呼叫write)會產生SIGPIPE訊號

close和shutdown函式的區別

#include <unistd.h>  
int close(int fd);  
  
#include <sys/socket.h>  
int shutdown(int sockfd, int how);  

shutdown的how引數

SHUT_RD

關閉讀端

SHUT_WR

關閉寫端

SHUT_RDWR

讀寫均關閉

close終止了資料傳送的兩個方向,shutdown可以有選擇的終止某個方向的資料傳送或者終止資料傳送的兩個方向

shutdowm how=1就可以保證對等方接收到一個EOF字元,而不管其他程序是否已經開啟可套接字。

而close不能保證,直到套接字引用計數減為0的時候才傳送。也就是說知道所有程序都關閉了套接字。

呼叫close函式,可能導致全雙工的管道還沒有回射給客戶端時,產生丟失資料現象;例如

FIN  D C B A 

A B C D ----》丟失,已經關閉close

Close準確的含義是  套接字引用計數減為的時候,才傳送FIN

int conn;
conn=accept(sock,NULL,NULL);
pid_t pid=fork();
if(pid==-1)
   ERR_EXIT("fork"); 
if(pid==0)
{
	close(sock);
	//通訊
	close(conn); //這時才會向對方傳送FIN段(因為這個時候conn引用計數減為0) 
}
else if(pid>0)
   close (conn); //不會向客戶端傳送FIN段,僅僅只是將套接字的引用計數減1