Linux socket編程入門及客戶端服務器端通信實現 – 基礎篇

摘要



隨着社會網絡化的發展,互聯網對人們的生活方式產生極大的影響,同時,也創造了一批互聯網企業,如著名的BAT。作爲一個IT程序員,學會網絡通信編程顯得十分重要,本文將詳細講解網絡編程API之一的套接字編程基本知識,同時充分利用Linux環境下的 Shell腳本和Makefile文件功能,實現一個簡單智能化的安裝配置。


程序安裝包下載

         資源下載


套接字地址結構

----------------------------------------------------------------------------------套接字地址結構-------------------------------------------------------------------------------------------------

一些概念

端口16位整數,TCPUDPSCTP用於區分不同的進程,分爲衆所周知的端口(1-1023),已登記的端口(1024-49151)和臨時端口(49152-65535

套接字標識每個端點的兩個值(IP地址和端口號),通常稱爲一個套接字。

TCP套接字對:定義連接的兩個端點四元組:本地地址IP,本地TCP端口號,外地IP地址,外地TCP端口號。套接字對唯一標識一個網絡上的每個TCP連接


套接字地址結構定義在 <netinet/in.h> 頭文件中

IPv4 結構:

struct  in_addr {
         in_addr_t  s_addr; // 32-bit IPv4 address, 網絡字節序
};

struct  sockaddr_in {
         uint8_t                        sin_len;   // 結構長度(16B),若無此項則 協議族爲2B
         sa_family_t               sin_family;// AF_INET
         in_port_t                    sin_port;  //16-bit 端口號
         struct  in_addr       sin_addr;  //32- bit IPv4 地址
         char                             sin_zeros[8];// 未使用

}

通用套接字地址結構(<sys/socket.h>)用於處理不同協議族的地址結構引用傳遞:

struct  sockaddr {
         uint8_t              sa_len;
         sa_family_t      sa_family; // 地址族:AF_XXX value
         char                    sa_data[14];
};


IPv6 結構:

struct  in6_addr {
         in_addr_t  s6_addr[16]; // 128-bit IPv6 address, 網絡字節序
};

#define SIN6_LEN 

struct  sockaddr_in6 {
         uint8_t                        sin6_len;   // 結構長度(28B)
         sa_family_t               sin6_family;// AF_INET6
         in_port_t                    sin6_port;  //16-bit 端口號
	 uint32_t                     sin6_flowinfo; //未定義
         struct  in6_addr     sin6_addr;  //128- bit IPv4 
         uint32_t                               sin6_scope_id;// 範圍地址

}

IPv6通用套接字地址結構(<netinet/in.h>)可容納系統支持的任何套接字地址結構:

struct  sockaddr_storage {
         uint8_t              sa_len;
         sa_family_t      sa_family; // 地址族:AF_XXX value

         /*
         …
         */

};


 

基本套接字函數

--------------------------------------------------------------------------基本套接字函數使用介紹---------------------------------------------------------------------------------------


基本的TCP套接字編程函數包括 socketconnectbindlistenaccept5個函數,需包含頭文件 #include <sys/socket.h>還有提供服務器併發功能的函數 fork exec函數族,和close函數(用於關閉套接字),包含頭文件 #include <unistd.h>

1     socket函數

函數說明:執行網絡I/O,一個進程必須做的一件事即調用socket函數,指定期望的通信協議類型

函數定義int  socket(int family, int type, int protocol);

         三個參數分別爲協議族,套接字類型,協議類型常值(通常設置爲0

IPv4協議用法sockfd =socket(AF_INET,SOCK_STREAM,0);

成功則返回非負描述符,即套接字描述符 sockfd,出錯返回 -1


2    connect函數

函數說明基於套接字描述符sockfd的函數,用來建立TCP服務器的連接,調用將激發TCP的三路握手過程,僅在連接建立成功或出錯時才返回

函數定義int  connect(int sockfd, conststruct sockaddr *servaddr,socklen_t addrlen);

         套接字描述符,套接字地址結構指針和該結構大小

IPv4協議用法if(connect(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr)) <0) exit(1);

成功返回0,出錯返回-1

 

3    bind函數

函數說明:基於套接字描述符sockfd的函數,用來把一個本地協議地址賦予一個套接字,如果不調用bind函數,內核則爲相應的套接字選擇一個臨時端口,但對於服務器來說,需要用bind綁定一個衆所周知的端口,這是很必要的。

函數定義int  bind(intsockfd, const struct sockaddr *myaddr, socklen_t addrlen);

         套接字描述符,指向特定於協議的地址結構指針和該地址結構長度

IPv4協議用法if(bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr))<0) exit(1);

成功返回0,出錯返回-1


4    listen函數

函數說明僅由TCP服務器調用,在服務器端,socket創建一個套接字時,被假設爲一個主動套接字,即一個將調用connect發起連接客戶套接字,listen函數把一個未連接的套接字轉換成一個被動套接字(等待客戶請求),指示內核應接受指向該套接字的連接請求。通常在調用socketbind函數之後,並在調用accept函數之前調用。

函數定義int   listen(int sockfd, int backlog) ;

         第二個參數規定了內核應該爲相應套接字排隊的最大連接個數。監聽套接字維護兩個隊列:未完成連接隊列SYN_RCVD)和已完成連接隊列ESTABLISHED),未完成連接隊列成員經過三路握手成功後可進入已完成連接隊列。

IPv4協議用法if(listen(sockfd,LISTEN))exit(1);

成功返回0,出錯返回-1


5    accept函數

函數說明:由TCP服務器調用,用於從已完成連接隊列隊頭返回下一個已完成連接,如果已完成連接隊列爲空,則進程被投入睡眠狀態(阻塞)

函數定義int   accpt(intsockfd, struct sockaddr *cliaddr,socklen_t *addrlen);

         第一個參數即套接字描述符稱爲監聽套接字(一般用listenfd代替sockfd,剩下倆個爲已連接的對端客戶進程的協議地址和長度,第三個參數爲-結果參數

IPv4協議用法connfd=accept(listenfd, (struct sockaddr*) &cliaddr, &len);;

若成功返回非負描述符,稱之爲已連接套接字(常用connfd表示),出錯則返回-1


-監聽套接字和已連接套接字的區別:一個服務器通常僅創建一個監聽套接字,並且在服務器週期內一直存在,內核爲每個由服務器進程接受的客戶連接創建一個已連接套接字(即TCP三路握手過程已經完成),當服務器完成對給定客戶服務時,相應的已連接套接字關閉。


6    fork函數和exec函數

函數說明:調用一次,返回兩次,即父進程和子進程,根據返回值判斷當前進程是父進程還是子進程。exec函數通常被fork子進程調用,然後將子進程替換成新的程序,被稱爲調用進程,具體可參見我之前總結的一篇對這兩個函數分析的文章

http://blog.csdn.net/gujinjinseu/article/details/25838381


函數定義pid_t   fork(void);


7   close函數

函數說明用於關閉套接字,並終止TCP連接

函數定義int   close(int sockfd);

成功返回0,出錯返回-1

注: 與shutdown 函數的區別,close 關閉可能是多連接的套接字,此時並不關閉fd,而shutdown則直接關閉fd (file descriptor)



------------------------------------------------------------------------------------TCP客戶/服務器程序示例-----------------------------------------------------------------------------

TCP服務器端程序

/* ==========================================================
			服務器端程序
   ==========================================================
   By	gujj	20140530
*/
   
#include <stdio.h>		// 標準輸入輸出流
#include <netinet/in.h> // socket struct
#include <sys/socket.h>	
#include <time.h>		// time
#include <string.h>		// htons()
#include <stdlib.h>		// exit()
#include <unistd.h>		//write()

#define MAXLINE 4096
#define LISTENQ 1024	// 監聽套接字最大連接數

int main(int argc, char **argv)
{
	// 聲明定義監聽套接字,已連接套接字
	int listenfd, connfd,n;
	// 被動套接字 
	struct sockaddr_in servaddr;
	char buff[MAXLINE];
	time_t ticks;
	
	/* 監聽套接字函數 */
	if(( listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
	{
		printf("socket error! \n");
		exit(1);
	}
	
	/* 初始化服務器端套接字地址結構 */
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 任意地址通配 */
	servaddr.sin_port = htons(13);	/* daytime 服務 */
	
	/* bind 函數 */
	if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
	{
		printf("bind error! \n");
		exit(1);
	}
	
	/* listen 函數 */
	// 將套接字變成 內核可以接受的形式
	if( listen(listenfd, LISTENQ) < 0 )
	{
		printf( "listen error! \n" );
		exit(1);
	}
	
	/* 循環處理客戶請求並反饋 */
	for( ; ; )
	{
		if((connfd = accept(listenfd,(struct sockaddr *)NULL,NULL))<0)
		{
			printf("accept error! \n");
			exit(1);
		}
		
		ticks = time(NULL);
		snprintf(buff, sizeof(buff), "%24s\r\n", ctime(&ticks));
		if((n = write(connfd,buff,strlen(buff)))<0)
		{
			printf("write error! \n");
			exit(1);			
		}
		
		if(close(connfd)<0)
		{
			printf("accept error! \n");
			exit(1);			
		}		
	}
	
}


TCP客戶端程序

/* ==========================================================
			客戶端程序
   ==========================================================
   By	gujj	20140530
*/
   
#include <stdio.h>		// 標準輸入輸出流
#include <netinet/in.h> // socket struct
#include <sys/socket.h>	
#include <arpa/inet.h>	// inet_pton()
#include <string.h>		// htons()
#include <stdlib.h>		// exit()
#include <unistd.h>		// read()

#define MAXLINE 4096

int main(int argc, char **argv)
{
	int sockfd,n;
	char recvline[MAXLINE + 1]; // 最後一個結束符
	struct sockaddr_in servaddr;
	
	/* 參數個數判斷 */
	if(argc != 2)
	{
		printf("Usage: a.out <IPaddress> \n");
		exit(1);
	}
	
	/* 套接字函數調用 */
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
	{
		printf("socket error! \n");
		exit(1);
	}
	
	/* 初始化服務器端套接字地址結構 */
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(13);
	
	/* inet_pton 點分十進制到網絡字節序二進制地址序列轉換 */
	// =0 表示輸入地址錯誤, <0 表示出錯
	if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
	{
		printf("inet_pton error for %s \n", argv[1]);
		exit(1);
	}
	
	/* connect 函數 */
	if(connect(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))<0)
	{
		printf("connect error! \n");
		exit(1); 
	}
	
	/* read 函數 */
	// n=0 關閉,n>0 讀取字節數, <0 失敗
	while(( n = read(sockfd, recvline, MAXLINE)) > 0 )
	{
		recvline[n] = 0; // 最後一個字節 添加 NULL
		if(fputs(recvline,stdout) == EOF)
		{
			printf("fputs error \n");
			exit(1);
		}
	}
	if(n<0)
	{
		printf("read error \n");
		exit(1);
	}
	
	exit(0);
	
}

Makefile 文件

#---------------------------------------
# Generate Server and Client for TCP
#---------------------------------------
# By Gu Jinjin

# multi-target
all: client server

client: tcpclient.o
	gcc -o client tcpclient.o

server: tcpserver.o
	gcc -o server tcpserver.o

# compile
tcpclient.o: tcpclient.c
	gcc -c -g tcpclient.c
	
tcpserver.o: tcpserver.c
	gcc -c -g tcpserver.c

# define the pseudo-target
.PHONY : cleanall clean

cleanall: clean
	rm -rf client server

clean:
	rm -rf tcpclient.o tcpserver.o
	


自動化配置Shell腳本

#! /bin/bash

# By Gu Jinjin
# Install client & server for TCP
# Debug or Test function
# Uninstall
# Note: if you edit in NotePad++, please use UTF-8 without BOM style

if [ $# -ne 1 ];then
	echo "\t------------------------------------"
	echo "\tUsage: sh setup <args>"
	echo "\t args are Numbers as follows:"
	echo "\t 1. make, generate executions"
	echo "\t 2. run, make & run"
	echo "\t 3. make clean, rm *.o"
	echo "\t 4. make cleanall, rm files in 1&2"
	echo "\t----------------------"
	echo "\t Exp: sh setup.sh 2 "
	echo "\t----------------------"
	echo "\t------------------------------------"
	exit 1
fi


case $1 in
	
	1)
	make > info.make 2>&1
	;;
	
	2)
	make > info.make 2>&1
	make clean
	path=`echo $PWD`
	sudo $PWD/server &
	#$PWD/client 127.0.0.1
	
	;;
	
	3)
	make clean
	;;
	
	4)
	sudo pkill server
	make cleanall
	rm -rf info.make
	;;
	
	*)
	echo "\tInput Wrong Args, please use command: "
	echo "\t\tsh setup.sh"
	echo "\tfor more Info!"
	;;
	
esac

exit 0


運行示例

編寫腳本實現工程的編譯連接安裝並運行,是Linux系統下的一大特點和優勢,本文實現一個簡單的智能化腳本,提供基於TCP協議的客戶端服務器端運行測試,以及刪除選項,具體參見下圖說明,這裏解釋一下: 1 即make爲運行make命令編譯 client 和server, 2. 即包括1中內容和運行服務器端,在後臺運行, 3. 刪除編譯產生的 .o文件,4. 終止服務器端後臺運行,並刪除所有文件


參考

UNIX網絡編程卷1(第三版)

高級Bash腳本編程指南(Appendix Q, 楊春敏,黃毅 譯)

Makefile:http://www.chinaunix.net/old_jh/23/408225.html







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