摘要
隨着社會網絡化的發展,互聯網對人們的生活方式產生極大的影響,同時,也創造了一批互聯網企業,如著名的BAT。作爲一個IT程序員,學會網絡通信編程顯得十分重要,本文將詳細講解網絡編程API之一的套接字編程基本知識,同時充分利用Linux環境下的 Shell腳本和Makefile文件功能,實現一個簡單智能化的安裝配置。
程序安裝包下載
套接字地址結構
----------------------------------------------------------------------------------套接字地址結構-------------------------------------------------------------------------------------------------
一些概念:
端口:16位整數,TCP,UDP和SCTP用於區分不同的進程,分爲衆所周知的端口(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套接字編程函數包括 socket,connect,bind,listen,accept等5個函數,需包含頭文件 #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函數把一個未連接的套接字轉換成一個被動套接字(等待客戶請求),指示內核應接受指向該套接字的連接請求。通常在調用socket,bind函數之後,並在調用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