Linux學習之socket編程(一)

socket編程

socket的概念:  

  在TCP/IP協議中,「IP地址+TCP或UDP端口號」惟一標識網絡通信中的一個進程,「IP地址+端口號」就稱爲socket。
  在TCP協議中,創建鏈接的兩個進程各自有一個socket來標識,那麼這兩個socket組成的socket pair就惟一標識一個鏈接。socket自己有「插座」的意思,所以用來描述網絡連
接的一對一關係。
  TCP/IP協議最先在BSD UNIX上實現,爲TCP/IP協議設計的應用層編程接口稱爲socketAPI。
  本章的主要內容是socket API,主要介紹TCP協議的函數接口,最後介紹UDP協議和UNIXDomain Socket的函數接口。linux

socket編程

socket-apiapi

1.基礎知識

網絡字節序  

  咱們已經知道,內存中的多字節數據相對於內存地址有大端和小端之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大端小端之分。網絡數據流一樣有大端小端之分,那麼如何定義網絡數據流的地址呢?發送主機一般將發送緩衝區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩衝區中,也是按內存地址從低到高的順序保存,所以,網絡數據流的地址應這樣規定:先發出的數據是低地址,後發出的數據是高地址。
  TCP/IP協議規定,網絡數據流應採用大端字節序,即低地址高字節。例如上一節的UDP段格式,地址0-1是16位的源端口號,若是這個端口號是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發0x03,再發0xe8,這16位在發送主機的緩衝區中也應該是低地址存0x03,高地址存0xe8。可是,若是發送主機是小端字節序的,這16位被解釋成0xe803,而不是1000。所以,發送主機把1000填到發送緩衝區以前須要作字節序的轉換。一樣地,接收主機若是是小端字節序的,接到16位的源端口號也要作字節序的轉換。若是主機是大端字節序的,發送和接收都不須要作轉換。同理,32位的IP地址也要考慮網絡字節序和主機字節序的問題。
  爲使網絡程序具備可移植性,使一樣的C代碼在大端和小端計算機上編譯後都能正常運行,能夠調用如下庫函數作網絡字節序和主機字節序的轉換。服務器

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示host,n表示network,l表示32位長整數,s表示16位短整數。
若是主機是小端字節序,這些函數將參數作相應的大小端轉換而後返回,若是主機是大端字節序,這些函數不作轉
換,將參數原封不動地返回。

IP地址轉換函數

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);//把字符串的ip轉換成32位二進制的整型
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//把32位二進制的整型轉成字符串的ip
轉換成
支持IPv4和IPv6 可重入函數

  其中inet_pton和inet_ntop不只能夠轉換IPv4的in_addr,還能夠轉換IPv6的in6_addr,所以函數接口是void *addrptr網絡

sockaddr數據結構

  strcut sockaddr 不少網絡編程函數誕生早於IPv4協議,那時候都使用的是sockaddr結構體,爲了向前兼容,如今sockaddr退化成了(void *)的做用,傳遞一個地址給函數,至於這個函數是sockaddr_in仍是sockaddr_in6,由地址族肯定,而後函數內部再強制類型轉化爲所需的地址類型

數據結構

 



sockaddr數據結構dom

struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

//ipv4
struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];//填充字節 }; /* Internet address. */ struct in_addr { __be32 s_addr; };

//ipv6
struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ }; struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 }; #define UNIX_PATH_MAX 108 struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname 相似於有名管道*/ };

 

Pv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位端口號和32位IP地址,IPv6地址用sockaddr_in6結構體表示,包括16位端口號、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定義在sys/un.h中,用sock-addr_un結構體表示。各類socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(並非全部UNIX的實現都有長度字段,如Linux就沒有),後16位表示地址類型。IPv四、IPv6和Unix Domain Socket的地址類型分別定義爲常數AF_INET、AF_INET六、AF_UNIX。這樣,只要取得某種sockaddr結構體的首地址,不須要知道具體是哪一種類型的sockaddr結構體,就能夠根據地址類型字段肯定結構體中的內容。所以,socket API能夠接受各類類型的sockaddr結構體指針作參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void *類型以便接受各類類型的指針,可是sock API的實現早於ANSI C標準化,那時尚未void *類型,所以這些函數的參數都用struct sockaddr *類型表示,在傳遞參數以前要強制類型轉換一下,例如:socket

struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));//傳遞參數時強轉  

2.網絡套接字函數

socket(構造出一條通道)

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain:
  AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
  AF_INET6 與上面相似,不過是來用IPv6的地址
  AF_UNIX 本地協議,使用在Unix和Linux系統上,通常都是當客戶端和服務器在同一臺及其上的時候使用
type:   SOCK_STREAM 這個協議是按照順序的、可靠的、數據完整的基於字節流的鏈接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。   SOCK_DGRAM 這個協議是無鏈接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的鏈接。   SOCK_SEQPACKET 這個協議是雙線路的、可靠的鏈接,發送固定長度的數據包進行傳輸。必須把這個包完整的接受才能進行讀取   SOCK_RAW 這個socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使用該協議)   SOCK_RDM 這個類型是不多使用的,在大部分的操做系統上沒有實現,它是提供給數據鏈路層使用,不保證數包的順序 protocol:   
0 默認協議 返回值:   成功返回一個新的文件描述符,失敗返回-1,設置errno

   socket()打開一個網絡通信端口,若是成功的話,就像open()同樣返回一個文件描述符,應用程序能夠像讀寫文件同樣用read/write在網絡上收發數據,若是socket()調用出錯則返回-1。對於IPv4,domain參數指定爲AF_INET。對於TCP協議,type參數指定爲SOCK_STREAM,表示面向流的傳輸協議。若是是UDP協議,則type參數指定爲SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的介紹從略,指定爲0便可。tcp

bind

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//綁定後,如有用戶訪問addr地址時,會經過sockfd進行數據傳遞 sockfd: socket文件描述符 addr: 構造出IP地址加端口號 addrlen:
sizeof(addr)長度 返回值: 成功返回0,失敗返回-1, 設置errno

  服務器程序所監聽的網絡地址和端口號一般是固定不變的,客戶端程序得知服務器程序的地址和端口號後就能夠向服務器發起鏈接,所以服務器須要調用bind綁定一個固定的網絡地址和端口號。
  bind()的做用是將參數sockfd和addr綁定在一塊兒,使sockfd這個用於網絡通信的文件描述符監聽addr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類型,addr參數實際上能夠接受多種協議的sockaddr結構體,而它們的長度各不相同,因此須要第三個參數addrlen指定結構體的長度。函數

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本機的任意一個IP地址均可以
servaddr.sin_port = htons(8000);

  首先將整個結構體清零,而後設置地址類型爲AF_INET,網絡地址爲INADDR_ANY,這個宏表示本地的任意IP地址,由於服務器可能有多個網卡,每一個網卡也可能綁定多個IP地址,這樣設置能夠在全部的IP地址上監聽,直到與某個客戶端創建了鏈接時才肯定下來到底用哪一個IP地址,端口號爲8000。
listen

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);//讓sockfd所指向的socktet具備監聽的能力

sockfd:
    socket文件描述符
backlog:
    排隊創建3次握手隊列和剛剛創建3次握手隊列的連接數和(默認爲128  )

 

  查看系統默認backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

  典型的服務器程序能夠同時服務於多個客戶端,當有客戶端發起鏈接時,服務器調用的accept()返回並接受這個鏈接,若是有大量的客戶端發起鏈接而服務器來不及處理,還沒有accept的客戶端就處於鏈接等待狀態,listen()聲明sockfd處於監聽狀態,而且最多容許有backlog個客戶端處於鏈接待狀態,若是接收到更多的鏈接請求就忽略。listen()成功返回0,失敗返回-1。

  若客戶端(ip加端口)向服務器發起連接,如下這些過程都是在內核進行的,在通過bind和socket函數後,服務器建立出一個socket(和ip+端口號綁定),在創建鏈接時候,TCP是經過三次握手創建,內核中會出兩個隊列,一個剛剛3次握手成功,另個是等待3次握手(三次握手整個過程沒有徹底完成)。隊列長度有限,若隊列滿了,再來信號,報錯RST。accept阻塞在socktet,監聽等待。若accept接受到鏈接,返回一個socket的文件描述符,專門用於和發起連接的客戶端通訊。connect負責發起鏈接,創建一個socket(會臨時分配一個端口號),向服務端發數據。

accept

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:

socket文件描述符
addr:
    傳出參數,返回連接客戶端地址信息,含IP地址和端口號
addrlen:
    傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小(IPv4或IPv6)
返回值:
    成功返回一個新的socket文件描述符,用於和客戶端通訊,失敗返回-1,設置errno

 

  三方握手完成後,服務器調用accept()接受鏈接,若是服務器調用accept()時尚未客戶端的鏈接請求,就阻塞等待直到有客戶端鏈接上來。addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-resultargument),傳入的是調用者提供的緩衝區addr的長度以免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調用者提供的緩衝區)。若是給addr參數傳NULL,表示不關心客戶端的地址。
  服務器程序結構是這樣的:

while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr,     &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ......
    close(connfd);
}

 

 connect

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockdf:
    socket文件描述符
addr:
    傳入參數,指定服務器端地址信息,含IP地址和端口號
addrlen:
    傳入參數,傳入sizeof(addr)大小
返回值:
    成功返回0,失敗返回-1,設置errno

 

  客戶端須要調用connect()鏈接服務器,connect和bind的參數形式一致,區別在於bind的參數是本身的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。

3.C/S模型-TCP

 

TCP協議通訊流程

  服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
  數據傳輸的過程:
  創建鏈接後,TCP協議提供全雙工的通訊服務,可是通常的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。所以,服務器從accept()返回後馬上調用read(),讀socket就像讀管道同樣,若是沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。

  若是客戶端沒有更多的請求了,就調用close()關閉鏈接,就像寫端關閉的管道同樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了鏈接,也調用close()關閉鏈接。注意,任何一方調用close()後,鏈接的兩個傳輸方向都關閉,不能再發送數據了。若是一方調用shutdown()則鏈接處於半關閉狀態,仍可接收對方發來的數據。

  在學習socket API時要注意應用程序和TCP協議層是如何交互的: *應用程序調用某個socket函數時TCP協議層完成什麼動做,好比調用connect()會發出SYN段 *應用程序如何知道TCP協議層的狀態變化,好比從某個阻塞的socket函數返回就代表TCP協議收到了某些段,再好比read()返回0就代表收到了FIN段。

實例:

mkdir server_test

touch server.c

touch client.c

touch Makefile

 

 

server.c

#include <sys/types.h>      
#include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #define SERVER_PORT 8000 #define MAXLINE 4096 int main(void) { struct sockaddr_in serveraddr, clientaddr; int sockfd, addrlen, confd, len, i; char ipstr[128]; char buf[MAXLINE]; //1.socket sockfd = socket(AF_INET, SOCK_STREAM, 0); //2.bind bzero(&serveraddr, sizeof(serveraddr)); /* 地址族協議IPv4 */ serveraddr.sin_family = AF_INET; /* IP地址 */ serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERVER_PORT); bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); //3.listen listen(sockfd, 128); while (1) { //4.accept阻塞監聽客戶端連接請求 addrlen = sizeof(clientaddr); confd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);//返回的是客戶端和服務端專用通道的socket描述符 //輸出客戶端IP地址和端口號 inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr)); printf("client ip %s\tport %d\n", inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr)), ntohs(clientaddr.sin_port)); //和客戶端交互數據操做confd //5.處理客戶端請求 len = read(confd, buf, sizeof(buf)); i = 0; while (i < len) { buf[i] = toupper(buf[i]); i++; } write(confd, buf, len); close(confd); } close(sockfd); return 0; }

 

nc  ip 端口
鏈接

 

client.c

#include <netinet/in.h>
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #define SERVER_PORT 8000 #define MAXLINE 4096 int main(int argc, char *argv[]) { struct sockaddr_in serveraddr; int confd, len; char ipstr[] = "192.168.6.254"; char buf[MAXLINE]; if (argc < 2) { printf("./client str\n"); exit(1); } //1.建立一個socket confd = socket(AF_INET, SOCK_STREAM, 0); //2.初始化服務器地址 bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; //"192.168.6.254" inet_pton(AF_INET, ipstr, &serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(SERVER_PORT); //3.連接服務器 connect(confd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); //4.請求服務器處理數據 write(confd, argv[1], strlen(argv[1])); len = read(confd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); //5.關閉socket  close(confd); return 0; }

 Makefile

all:server client


server:server.c
    gcc $< -o $@

client:client.c
    gcc $< -o $@


.PHONY:clean
clean:
    rm -f server
    rm -f client

 

  因爲客戶端不須要固定的端口號,所以沒必要調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不容許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但若是服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啓動服務器時端口號都不同,客戶端要鏈接服務器就會遇到麻煩。客戶端和服務器啓動後能夠查看連接狀況:

netstat -apn|grep 8000

(來源傳智播客邢文鵬linux系統編程的筆記)

相關文章
相關標籤/搜索