Linux下的socket程式設計實踐(四)TCP服務端優化和常見函式
併發下的殭屍程序處理
只有一個程序連線的時候,我們可以使用以下兩種方法處理殭屍程序:
1)通過忽略SIGCHLD訊號,避免殭屍程序
在server端程式碼中新增
signal(SIGCHLD, SIG_IGN);
2)通過wait/waitpid方法,解決殭屍程序
signal(SIGCHLD,onSignalCatch);
void onSignalCatch(int signalNumber)
{
wait(NULL);
}
那麼如果是多程序狀態下多個客戶端同時關閉呢?
我們可以用下面的客戶端程式碼測試:
此時由於訊號的同時到達,並且SIGCHLD又是不可靠訊號,不支援排隊,會留下相當部分的殭屍程序/** 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); }
解決方法:
使用迴圈的 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 |
讀寫均關閉 |
shutdowm how=1就可以保證對等方接收到一個EOF字元,而不管其他程序是否已經開啟可套接字。
而close不能保證,直到套接字引用計數減為0的時候才傳送。也就是說知道所有程序都關閉了套接字。
呼叫close函式,可能導致全雙工的管道還沒有回射給客戶端時,產生丟失資料現象;例如
FIN D C B A
A B C D ----》丟失,已經關閉close
Close準確的含義是 套接字引用計數減為0 的時候,才傳送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