天天看點

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client



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的函數接口。

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client

圖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

函數聲明

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位短整數。

如果主機是小端位元組序,這些函數将參數做相應的大小端轉換然後傳回,如果主機是大端位元組序,這些函數不做轉換,将參數原封不動地傳回。

名稱:

htonl

功能:

the htonl() function converts the unsigned integer hostlong 

from  host byte order to network byte order

頭檔案:

函數原形:

參數:

傳回值:

htons

the htons() function converts the unsigned short integer hostshort from

host byte order to network byte order.

ntohl

the ntohl() function converts the unsigned integer netlong from network

byte order to host byte order.

ntohs

the ntohs() function converts the unsigned short integer netshort from

network byte order to host byte order.

4 ip位址轉換函數

#include <sys/socket.h>

#include <netinet/in.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

現在

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,由位址族确定,然後函數内部再強制類型轉

化為所需的位址類型

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client

圖 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>

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:

預設協定

成功傳回一個新的檔案描述符,失敗傳回-1,設定errno

socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣傳回一個檔案描述符,應用程式可以像讀寫檔案一樣用read/write在網絡上收發資料,如果socket()調用出錯則傳回-1。對于ipv4,domain參數指定為af_inet。對于tcp協定,type參數指定為sock_stream,表示面向流的傳輸協定。如果是udp協定,則type參數指定為sock_dgram,表示面向資料報的傳輸協定。protocol參數的介紹從略,指定為0即可。

7 bind

#include <sys/types.h> /* see notes*/

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指定結構體的長度。如:

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

int listen(int sockfd, int backlog);

socket檔案描述符

backlog:

排隊建立3次握手隊列和剛剛建立3次握手隊列的連結數和

檢視系統預設backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的伺服器程式可以同時服務于多個用戶端,當有用戶端發起連接配接時,伺服器調用的

accept()傳回并接受這個連接配接,如果有大量的用戶端發起連接配接而伺服器來不及處理,尚未

accept的用戶端就處于連接配接等待狀态,listen()聲明sockfd處于監聽狀态,并且最多允許有

backlog個用戶端處于連接配接待狀态,如果接收到更多的連接配接請求就忽略。listen()成功傳回

0,失敗傳回-1。

9

 accept

int accept(int sockfd, struct sockaddr*addr, socklen_t *addrlen);

sockdf:

傳出參數,傳回連結用戶端位址資訊,含ip位址和端口号

傳入傳出參數(值-結果),傳入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

int connect(int sockfd, const structsockaddr *addr, socklen_t addrlen);

傳入參數,指定伺服器端位址資訊,含ip位址和端口号

傳入參數,傳入sizeof(addr)大小

用戶端需要調用connect()連接配接伺服器,connect和bind的參數形式一緻,差別在于bind的參數是自己的位址,而connect的參數是對方的位址。connect()成功傳回0,出錯傳回-1。

11 c/s模型-tcp

下圖是基于tcp協定的用戶端/伺服器程式的一般流程:

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client

圖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 <stdio.h>

#include <string.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 <stdlib.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(int argc,char *argv[])

struct sockaddr_in serveraddr;

int confd,len;

char ipstr[] = "192.168.6.14";

if(argc < 2) {

printf("./client str\n");

exit(1);

//1、建立一個socket

confd = socket(af_inet,sock_stream,0);

//2、初始化伺服器位址

//"192.168.6.14"

inet_pton(af_inet,ipstr,&serveraddr.sin_addr.s_addr);

//3.連結伺服器處理資料

connect(confd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

//4.請求伺服器處理資料

write(confd,argv[1],strlen(argv[1]));

write(stdout_fileno,buf,len);

//關閉socket

makefile

all:server client

server:server.c

gcc $< -o $@

client:client.c

.phony:clean

clean:

rm -f server

rm -f client

運作:

在終端上輸入make,先啟動server端,在啟動client,在啟動用戶端的時候同時輸入字元串。

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client

用戶端運作效果:

1.socket程式設計:socket程式設計,網絡位元組序,函數介紹,IP位址轉換函數,sockaddr資料結構,網絡套接字函數,socket相關函數,TCP server和client

繼續閱讀