chapter04_基本TCP套接字編程

chapter_04 傳輸層:基本TCP套接字編程

這篇文章是我自己看的。閱讀《unix網絡編程》,整理自己的思路,僅放在網上便於保存&&分享。

相對於書而言,內容沒有什麼價值。書上的內容全面可靠。

PS:文章是建立在我的知識體系之上。因而,文中也不會有多餘解釋。(如果有不清楚的閱讀書本/google)

必要的內容放在正文中。相對而言非主線的內容放在附錄中。這兩個集合之外的內容,可能沒有寫出,或者隨它而去。

爲了好看增加顏色標識。提問內容顏色標識:綠色; 重點程度顏色標識:紅色>藍色>黑體。

(csdn自身的顏色不在考慮範圍內。)


一、內容

1、概述

本章講解編寫一個完整的TCP客戶/服務器程序所需要的基本套接字函數。



2、基本套接字函數

整體的代碼,見代碼部分。

2.1、socket函數

函數原型: int socket(int family, int type, int protocol);

eg:int sockfd = socket(AF_INET, SOCK_STREAM, 0)

函數作用:指定期望的通信協議類型

參數解析:

  • family參數指明協議族
  • type參數指明套接字類型
  • protocol參數應設爲某個協議類型常值, 或者設爲0, 以選擇所給定family和type組合的系統默認值。

參數表:

其中標爲“是”的項也是有效的, 但還沒有找到便捷的縮略詞。 而空白項則是無效組合。

在這裏插入圖片描述

返回值:

socket函數在成功時返回一個小的非負整數值, 它與文件描述符類似, 我們把它稱爲套接字描述符(socket descriptor) , 簡稱sockfd。


2.2 、connect函數

函數原型:int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

eg:connect(sockfd, (SA *) &servaddr, sizeof(servaddr))

函數作用:TCP客戶用connect函數來建立與TCP服務器的連接。

參數解析:

  • sockfd是由socket函數返回的套接字描述符
  • 第二個、 第三個參數分別是一個指向套接字地址結構的指針和該結構的大小

函數返回:若成功則爲0, 若出錯則爲-1

出錯原因:

  • 若TCP客戶沒有收到SYN分節的響應, 則返回ETIMEDOUT錯誤。
  • 若對客戶的SYN的響應是RST(表示復位) , 則表明該服務器主機在我們指定的端口上沒有進程在等待與之連接(例如服務器進程也許沒在運行) 。 這是一種硬錯誤(hard error) , 客戶一接收到RST就馬上返回ECONNREFUSED錯誤。
  • 若客戶發出的SYN在中間的某個路由器上引發了一個“destinationunreachable”(目的地不可達) ICMP錯誤, 則認爲是一種軟錯誤(softerror) 。 客戶主機內核保存該消息, 並按第一種情況中所述的時間間隔繼續發送SYN。 若在某個規定的時間(4.4BSD規定75s) 後仍未收到響應,則把保存的消息(即ICMP錯誤) 作爲EHOSTUNREACH或ENETUNREACH錯誤返回給進程

2.3、bind函數

函數原型:int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

eg:Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

函數作用:把一個本地協議地址賦予一個套接字。

函數作用解析:

  • servaddr是存儲着協議地址;listenfd是套接字描述符,由socket產生;

  • 調用bind可以指定IP地址或端口, 可以兩者都指定,也可以都不指定。 如果指定端口號爲0, 那麼內核就在bind被調用時選擇一個臨時端口。然而如果指定IP地址爲通配地址, 那麼內核將等到套接字已連接(TCP)或已在套接字上發出數據報(UDP) 時才選擇一個本地IP地址。

  • 進程可以把一個特定的IP地址捆綁到它的套接字上, 不過這個IP地址必須屬於其所在主機的網絡接口之一。 對於TCP客戶, 這就爲在該套接字上發送的IP數據報指派了源IP地址。 對於TCP服務器, 這就限定該套接字只接收那些目的地爲這個IP地址的客戶連接(即監聽綁定的這個ip:port)。 TCP客戶通常不把IP地址捆綁到它的套接字上。 當連接套接字時, 內核將根據所用外出網絡接口來選擇源IP地址, 而所用外出接口則取決於到達服務器所需的路徑(TCPv2第737頁) 。 如果TCP服務器沒有把IP地址捆綁到它的套接字上, 內核就把客戶發送的SYN的目的IP地址作爲服務器的源IP地址(TCPv2第943頁) 。

  • 也就是說

    客戶端套接字:未使用bind
    套接字裏面已經含有IP/端口信息了。但信息是對方服務器的信息。我本身並不需要知道,信息從哪個端口出去。
    服務器套接字:使用bind
    得套接字裏面的IP=INADDR_ANY=0 ,如果有幾張網卡,這幾張網卡的IP都算在內。端口號爲固定(假設是9999)。這個端口是監聽端口,得固定。

函數參數:

  • sockfd是由socket函數返回的套接字描述符
  • 第二個、 第三個參數分別是一個指向套接字地址結構的指針和該結構的大小

函數返回:若成功則爲0, 若出錯則爲-1


2.4 、listen函數

函數原型:int listen(int sockfd, int backlog);

eg:Listen(listenfd, LISTENQ);

函數作用: listen函數把一個未連接的套接字轉換成一個被動套接字, 指示內核應接受指向該套接字的連接請求。

函數參數:

  • sockfd是由socket函數返回的套接字描述符

  • 函數的第二個參數規定了內核應該爲相應套接字排隊的最大連接個數=未完成連接隊列+已完成連接隊列。

    黑客編寫了一個以高速率給受害主機發送SYN的程序, 用以裝填一個或多個TCP端口的未完成連接隊列。 這樣, 通過以僞造的SYN裝
    填未完成連接隊列, 使合法的SYN排不上隊, 導致針對合法客戶的服務被
    拒絕(denial of service)

函數返回:若成功則爲0, 若出錯則爲-1


2.5 、accept函數

函數原型:int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

eg:connfd = accept(listenfd, (SA *) NULL, NULL);

函數作用: 用於從已完成連接隊列隊頭返回下一個已完成連接.

函數參數:

  • 第一個參數爲監聽套接字(listening socket) 描述符(由socket創建, 隨後用作
    bind和listen的第一個參數的描述符)
  • 函數的第二個參數可以用來返回客戶端的協議地址;第三個參數,返回該協議地址的長度。

函數返回:若成功則爲非負描述符我們稱它爲已連接套接字(connected socket) 描述符。 若出錯則爲-1


2.6 fork和exec函數

函數原型:pid_t fork(void);

函數作用:該函數是Unix中派生新進程的方法。

函數返回:在子進程中爲0, 在父進程中爲子進程ID, 若出錯則爲-1

fork有兩個典型用法:

  • 一個進程創建一個自身的副本, 這樣每個副本都可以在另一個副本執行其他任務的同時處理各自的某個操作。 這是網絡服務器的典型用法。
  • 一個進程想要執行另一個程序。 既然創建新進程的唯一辦法是調用fork, 該進程於是首先調用fork創建一個自身的副本, 然後其中一個副本(通常爲子進程) 調用exec(接下去介紹) 把自身替換成新的程序。 這是諸如shell之類程序的典型用法。

2.7 close函數

函數原型:int close(int sockfd);

函數功能:關閉套接字, 並終止TCP連接

函數返回:若成功則爲0, 若出錯則爲-1


2.8、getsockname和getpeername函數

getsockname函數原型:int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);

函數功能:返回與某個套接字關聯的本地協議地址

函數參數:同上

注:在這樣的調用中, 套接字描述符參數必須是已連接套接字的描述符, 而不是監聽套接字的描述符

函數返回:若成功則爲0, 若出錯則爲-1

getpeername函數原型:int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

函數功能:返回與某個套接字關聯的外地協議地址

函數參數:同上

注:該函數應該適合任何已打開的套接字描述符。

函數返回:若成功則爲0, 若出錯則爲-1


2.9、整體過程

在這裏插入圖片描述



二、代碼

在源代碼的基礎上進行修改。也是課後習題4.2的代碼。

1、客戶端代碼

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd, n;
	char				recvline[MAXLINE + 1];
	struct sockaddr_in	servaddr;
	struct sockaddr_in  client_addr;
	socklen_t client_addr_len=0;

	if (argc != 2)
		err_quit("usage: a.out <IPaddress>");

	if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		err_sys("socket error");

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port   = htons(13);	/* daytime server */
	if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
		err_quit("inet_pton error for %s", argv[1]);

	if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
		err_sys("connect error");
	
	if(getsockname(sockfd,(SA *)&client_addr,&client_addr_len)<0) 
    	return -1;      
	printf("client address is %s:%d\n",inet_ntop(AF_INET,&client_addr.sin_addr,recvline,sizeof(client_addr)), 
	 									ntohs(client_addr.sin_port));

	// while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
	// 	recvline[n] = 0;	/* null terminate */
	// 	if (fputs(recvline, stdout) == EOF)
	// 		err_sys("fputs error");
	// }
	
	//最多可以讀取(MAXLINE + 1)字符。不夠,提前終止;
	//返回讀取的字符數;
	//因爲已知,時間的字符串長度不長,足以存下。所以可以用readn
	//在未知長度的時候,用上面的寫法。
	n=readn(sockfd,(SA *) recvline, MAXLINE + 1);
	printf("%s",recvline);

	if (n < 0)
		err_sys("read error");

	exit(0);
}

2、服務端代碼

#include	"unp.h"
#include	<time.h>

int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	struct sockaddr_in	servaddr;
	char				buff[MAXLINE];
	time_t				ticks;

	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(13);	/* daytime server */

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	for ( ; ; ) {
		connfd = Accept(listenfd, (SA *) NULL, NULL);

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

        //Write(connfd, buff, strlen(buff));
		writen(connfd, buff, strlen(buff));

		Close(connfd);
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章