1.socket编程:socket编程,网络字节序,函数介绍,IP地址转换函数,sockaddr数据结构,网络套接字函数,socket相关函数,TCP server和client
1? Socket編程
socket這個詞可以表示很多概念:
在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程,“IP
地址+端口號”就稱為socket。
在TCP協議中,建立連接的兩個進程各自有一個socket來標識,那么這兩個socket組成的socket pair就唯一標識一個連接。socket本身有“插座”的意思,因此用來描述網絡連
接的一對一關系。
TCP/IP協議最早在BSD UNIX上實現,為TCP/IP協議設計的應用層編程接口稱為socket
API。
本章的主要內容是socketAPI,主要介紹TCP協議的函數接口,最后介紹UDP協議和UNIX Domain Socket的函數接口。
圖11.1:socketAPI
2 網絡字節序
我們已經知道,內存中的多字節數據相對于內存地址有大端和小端之分,磁盤文件中的
多字節數據相對于文件中的偏移地址也有大端小端之分。網絡數據流同樣有大端小端之分,
那么如何定義網絡數據流的地址呢?發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址。
??? 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代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
3 函數介紹
A 依賴的頭文件
#include <arpa/inet.h>
B 函數聲明
#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位短整數。
如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回,如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
?
uint32_t htonl(uint32_t hostlong);
| 名稱: | htonl |
| 功能: | The htonl() function converts the unsigned integer hostlong? from? host byte order to network byte order |
| 頭文件: | #include <arpa/inet.h> |
| 函數原形: | uint32_t htonl(uint32_t hostlong); |
| 參數: | ? |
| 返回值: | ? |
?
uint16_t htons(uint16_t hostshort);
| 名稱: | htons |
| 功能: | The htons() function converts the unsigned short integer hostshort from host byte order to network byte order. |
| 頭文件: | #include <arpa/inet.h> |
| 函數原形: | uint16_t htons(uint16_t hostshort); |
| 參數: | ? |
| 返回值: | ? |
?
uint32_t ntohl(uint32_t netlong);
| 名稱: | ntohl |
| 功能: | The ntohl() function converts the unsigned integer netlong from network byte order to host byte order. |
| 頭文件: | #include <arpa/inet.h> |
| 函數原形: | uint32_t ntohl(uint32_t netlong); |
| 參數: | ? |
| 返回值: | ? |
?
uint16_t ntohs(uint16_t netshort);
| 名稱: | ntohs |
| 功能: | The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order. |
| 頭文件: | #include <arpa/inet.h> |
| 函數原形: | uint16_t ntohs(uint16_t netshort); |
| 參數: | ? |
| 返回值: | ? |
?
4 IP地址轉換函數
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, structin_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
只能處理IPv4的ip地址
不可重入函數
注意參數是struct in_addr
?
現在
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void*dst);
const char *inet_ntop(int af, const void*src, char *dst, socklen_t size);
支持IPv4和IPv6
可重入函數
?
其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void*addrptr
?
5 sockaddr數據結構
??? strcutsockaddr 很多網絡編程函數誕生早于IPv4協議,那時候都使用的是sockaddr結
構體,為了向前兼容,現在sockaddr退化成了(void *)的作用,傳遞一個地址給函數,至
于這個函數是sockaddr_in還是sockaddr_in6,由地址族確定,然后函數內部再強制類型轉
化為所需的地址類型
圖 11.2:sockaddr數據結構
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
?
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;
};
?
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中,用sockaddr_un結構體表示。各種socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(并不是所有UNIX的實現都有長度字段,如Linux就沒有),后16位表示地址類型。IPv4、IPv6和Unix Domain Socket的地址類型分別定義為常數AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void *類型以便接受各種類型的指針,但是sock API的實現早于ANSI C標準化,那時還沒有void *類型,因此這些函數的參數都用struct sockaddr *類型表示,在傳遞參數之前要強制類型轉換一下,例如:
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr));
?
6 網絡套接字函數
A socket
#include <sys/types.h>
#include <sys/socket.h>
?
int socket(int domain,int types,intprotocol);
?
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即可。
?
7 bind
A 依賴的頭文件
#include <sys/types.h> /* See NOTES*/
#include <sys/socket.h>
B 函數聲明
int bind(int sockfd, const struct sockaddr*addr, socklen_t addrlen);
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);
servaddr.sin_port = htons(8000);
首先將整個結構體清零,然后設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為8000。
?
8 listen
#include <sys/types.h> /* See NOTES*/
#include <sys/socket.h>
?
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:
排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和
?
查看系統默認backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服務器程序可以同時服務于多個客戶端,當有客戶端發起連接時,服務器調用的
accept()返回并接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未
accept的客戶端就處于連接等待狀態,listen()聲明sockfd處于監聽狀態,并且最多允許有
backlog個客戶端處于連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回
0,失敗返回-1。
?
9 ?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)大小,函數返回時返回真正接收到地址結構體的大小
返回值:
成功返回一個新的socket文件描述符,用于和客戶端通信,失敗返回-1,設置errno
?
三方握手完成后,服務器調用accept()接受連接,如果服務器調用accept()時還沒有
客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數,accept()
返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩沖區addr的長度以避免緩沖區溢出問題,傳出的是客
戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給addr參數傳
NULL,表示不關心客戶端的地址。
?
我們的服務器程序結構是這樣的:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr,&cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
?
整個是一個while死循環,每次循環處理一個客戶端連接。由于cliaddr_len是傳入傳出
參數,每次調用accept()之前應該重新賦初值。accept()的參數listenfd是先前的監聽文件
描述符,而accept()的返回值是另外一個文件描述符connfd,之后與客戶端之間就通過這個
connfd通訊,最后關閉connfd斷開連接,而不關閉listenfd,再次回到循環開頭listenfd仍
然用作accept的參數。accept()成功返回一個文件描述符,出錯返回-1。
?
10 connect
#include <sys/types.h> /* See NOTES*/
#include <sys/socket.h>
int connect(int sockfd, const structsockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
傳入參數,指定服務器端地址信息,含IP地址和端口號
addrlen:
傳入參數,傳入sizeof(addr)大小
返回值:
成功返回0,失敗返回-1,設置errno
?
客戶端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在于bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。
?
11 C/S模型-TCP
??? 下圖是基于TCP協議的客戶端/服務器程序的一般流程:
圖11.3: 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段
12 TCP服務器客戶端案例說明
server.c
| #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <ctype.h> #include <unistd.h> ? #define SERVER_PORT 8000 #define MAXLINE 4096 ? int main(void) { ??? struct sockaddr_in serveraddr,clientaddr; ??? int sockfd,addrlen,confd,len,i; ??? //存儲ip地址 ??? char ipstr[128]; ??? char buf[MAXLINE]; ??? ??? //1.socket ??? //AF_INET:表示使用的是Ipv4協議,如果想使用ipv6,使用AF_INET6 ??? //SOCK_STREAM:表示使用的是TCP協議 ??? sockfd = socket(AF_INET,SOCK_STREAM,0); ??? //2.bind,bzero將內容清零 ??? 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,表示最大等待排隊的數量為128個 ??? listen(sockfd,128); ??? while(1) { ??????? //4.accept 阻塞監聽客戶端鏈接請求 ??????? addrlen = sizeof(clientaddr); ??????? confd = accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen); ??????? //輸出客戶端IP地址和端口號 ??????? inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)); ??????? //打印除ip地址和端口號 ??????? 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.處理客戶端請求,read,write默認是阻塞的。 ??????? len = read(confd,buf,sizeof(buf)); ??????? i = 0; ??????? while(i < len) { ??????????? buf[i] = toupper(buf[i]); ?????? ?????i++; ??????? } ??????? write(confd,buf,len); ??????? //發生里4次鏈接 ??????? close(confd); ??? } ??? close(sockfd); ? ??? return 0; } |
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.14"; ??? 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.14" ??? 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); ? ??? //關閉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 |
運行:
在終端上輸入make,先啟動server端,在啟動client,在啟動客戶端的時候同時輸入字符串。
客戶端運行效果:
總結
以上是生活随笔為你收集整理的1.socket编程:socket编程,网络字节序,函数介绍,IP地址转换函数,sockaddr数据结构,网络套接字函数,socket相关函数,TCP server和client的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么给pe设置开机密码 设置Pe开机密码
- 下一篇: 1高并发服务器:多进程服务器