三元組(ip位址,協定,端口)就可以辨別網絡的程序
3.1,OSI七層模型和TCP/IP五層模型
OSI七層網絡模型由下至上為1至7層,分别為:
實體層(Physical layer),資料鍊路層(Data link layer),網絡層(Network layer),傳輸層(Transport layer),會話層(Session layer),表示層(Presentation layer),應用層(Application layer)。
1.1 應用層,很簡單,就是應用程式。這一層負責确定通信對象,并確定由足夠的資源用于通信,這些當然都是想要通信的應用程式幹的事情。
1.2 表示層,負責資料的編碼、轉化,確定應用層的正常工作。這一層,是将我們看到的界面與二進制間互相轉化的地方,就是我們的語言與機器語言間的轉化。資料的壓縮、解壓,加密、解密都發生在這一層。這一層根據不同的應用目的将資料處理為不同的格式,表現出來就是我們看到的各種各樣的檔案擴充名。
1.3 會話層,負責建立、維護、控制會話,區分不同的會話,以及提供單工(Simplex)、半雙工(Half duplex)、全雙工(Full duplex)三種通信模式的服務。我們平時所知的NFS,RPC,X Windows等都工作在這一層。
1.4 傳輸層,負責分割、組合資料,實作端到端的邏輯連接配接。資料在上三層是整體的,到了這一層開始被分割,這一層分割後的資料被稱為段(Segment)。三次握手(Three-way handshake),面向連接配接(Connection-Oriented)或非面向連接配接(Connectionless-Oriented)的服務,流控(Flow control)等都發生在這一層。
1.5 網絡層,負責管理網絡位址,定位裝置,決定路由。我們所熟知的IP位址和路由器就是工作在這一層。上層的資料段在這一層被分割,封裝後叫做包(Packet),包有兩種,一種叫做使用者資料包(Data packets),是上層傳下來的使用者資料;另一種叫路由更新包(Route update packets),是直接由路由器發出來的,用來和其他路由器進行路由資訊的交換。
1.6 資料鍊路層,負責準備實體傳輸,CRC校驗,錯誤通知,網絡拓撲,流控等。我們所熟知的MAC位址和交換機都工作在這一層。上層傳下來的包在這一層被分割封裝後叫做幀(Frame)。
1.7 實體層,就是實實在在的實體鍊路,負責将資料以比特流的方式發送、接收,就不多說了。
3.2,TCP握手
3.2.1 三次握手建立連接配接
在TCP/IP協定中,TCP協定提供可靠的連接配接服務,采用三次握手建立一個連接配接。
1,用戶端向伺服器發送一個SYN J
2,伺服器向用戶端響應一個SYN K,并對SYN J進行确認ACK J+1
3,用戶端再想伺服器發一個确認ACK K+1

從圖中可以看出,當用戶端調用connect時,觸發了連接配接請求,向伺服器發送了SYN J包,這時connect進入阻塞狀态;伺服器監聽到連接配接請求,即收到SYN J包,調用accept函數接收請求向用戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀态;用戶端收到伺服器的SYN K ,ACK J+1之後,這時connect傳回,并對SYN K進行确認;伺服器收到ACK K+1時,accept傳回,至此三次握手完畢,連接配接建立。
三次握手的目的是建立雙向的連接配接,第一次握手是用戶端向伺服器端送出請求
第二次握手是伺服器端告訴用戶端,第一次握手是成功的,即可以從用戶端發送到用戶端,
第三次握手是用戶端告訴伺服器端,第二次握手是成功的,即可以從用戶端到伺服器端
這樣就保證了用戶端和伺服器端的雙向通信,
3.2.2 四次握手釋放連接配接
1,某個應用程序首先調用close主動關閉連接配接,這時TCP發送一個FIN M;
2,另一端接收到FIN M之後,執行被動關閉,對這個FIN進行确認。它的接收也作為檔案結束符傳遞給應用程序,因為FIN的接收意味着應用程序在相應的連接配接上再也接收不到額外資料;
3,一段時間之後,接收到檔案結束符的應用程序調用close關閉它的socket。這導緻它的TCP也發送一個FIN N;
4,接收到這個FIN的源發送端TCP對它進行确認。
3.3,socket程式設計
javascript:void(0)
先從伺服器端說起。伺服器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待用戶端連接配接。在這時如果有個用戶端初始化一個Socket,然後連接配接伺服器(connect),如果連接配接成功,這時用戶端與伺服器端的連接配接就建立了。用戶端發送資料請求,伺服器端接收請求并處理請求,然後把回應資料發送給用戶端,用戶端讀取資料,最後關閉連接配接,一次互動結束。
3.3.1 AF_INET TCP傳輸最簡單版本
tcp_client.c
[cpp] view plain copy
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <errno.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #define PORT 6666
- #define BUFF_SIZE 1024
- #define MAXLEN 4096
- main(int argc, char** argv)
- {
- if(argc!=2){
- printf("usage: ./client <ipaddress>\n");
- exit(0);
- }
- char sendline[4096];
- //socket()建立socket
- int sockfd = socket(AF_INET,SOCK_STREAM,0);
- //要連接配接的伺服器位址
- struct sockaddr_in sliaddr;
- sliaddr.sin_family = AF_INET;
- sliaddr.sin_port = htons(PORT);
- inet_pton(AF_INET, argv[1], &sliaddr.sin_addr);
- //connect()發送請求(ip=argv[1],protocal=TCP,port=6666)
- connect(sockfd, (struct sockaddr*)&sliaddr, sizeof(sliaddr));
- //recv sth
- recv_len = recv(sockfd, buff, sizeof(buff), 0);
- buff[recv_len] = '\0';
- printf(" %s ", buff);
- //interactive
- while (1)
- {
- printf("Enter string to send: ");
- scanf("%s", buff);
- if (!strcmp(buff, "quit"))
- break;
- send_len = send(sockfd, buff, strlen(buff), 0);
- recv_len = recv(sockfd, buff, BUFF_SIZE, 0);
- buff[recv_len] = '\0';
- printf(" received: %s \n", buff);
- //close()關閉連接配接
- close(sockfd);
- }
tcp_server.c
- #define WAIT_QUEUE_LEN 5
- #define WELCOME "Welcome to my server ^_^!\n"
- main()
- int connfd;
- char buff[MAXLEN];
- int len;
- //socket() 建立socket,其中SOCK_STREAM表示tcp連接配接
- struct sockaddr_in servaddr;
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(PORT);
- //bind()綁定一個socket(ip=all,protocal=TCP,port=6666)
- bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr));
- //listen()監聽
- listen(sockfd, WAIT_QUEUE_LEN);
- //accept() & close()
- printf("======waiting for client's request======\n");
- while(1){
- c_addrlen = sizeof(struct sockaddr_in);
- connfd = accept(serverfd, (struct sockaddr *)&caddr, &c_addrlen);
- printf("client: ip=%s,port=%s\n", cliaddr.sin_addr.s_addr,cliaddr.sin_port);
- //send a welcome
- send(connfd, WELCOME, strlen(WELCOME), 0);
- //阻塞模式下recv==0表示用戶端已斷開連接配接
- while ((len = recv(connfd, buff, BUFF_SIZE, 0)) > 0)
- {
- buff[len] = '\0';
- printf("recv msg is : %s \n", buff);
- send(connfd, buff, len, 0);
- }
- close(connfd);
阻塞與非阻塞recv傳回值沒有區分,都是
<0 出錯
=0 連接配接關閉
>0 接收到資料大小,
makefile
[plain] view plain copy
- .PHONY : main
- main : server client
- server : server.o
- gcc -g -o server server.o
- client : client.o
- gcc -g -o client client.o
- server.o : server.c
- gcc -g -c server.c
- client.o : client.c
- gcc -g -c client.c
- clean :
- rm -rf *.o
- ser :
- ./server
- cli :
- ./client
3.3.2 加入傳回值檢查和IP位址
- int sockfd;
- if((sockfd=socket(AF_INET,SOCK_STREAM,0)) ==-1){
- printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
- struct sockaddr_in cliaddr;
- cliaddr.sin_family = AF_INET;
- cliaddr.sin_port = htons(6666);
- if(inet_pton(AF_INET, argv[1], &cliaddr.sin_addr)==-1){
- printf("inet_pton error for %s\n",argv[1]);
- if(connect(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr))==-1){
- printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
- printf("send msg to server: \n");
- fgets(sendline, 4096, stdin);
- //send()發送資料
- if(send(sockfd, sendline, strlen(sendline), 0)==-1){
- printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
- int n;
- if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
- printf("create socket error: %s(errno:%d)\n",strerror(errno),errno);
- servaddr.sin_port = htons(6666);
- if(bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr))==-1){
- printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
- if(listen(sockfd,10)==-1){
- printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
- if((connfd=accept(sockfd, (struct sockaddr *)NULL,NULL))==-1){
- printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
- continue;
- struct sockaddr_in serv, guest;
- char serv_ip[20];
- char guest_ip[20];
- int serv_len = sizeof(serv);
- int guest_len = sizeof(guest);
- getsockname(connfd, (struct sockaddr *)&serv, &serv_len);
- getpeername(connfd, (struct sockaddr *)&guest, &guest_len);
- inet_ntop(AF_INET, &serv.sin_addr, serv_ip, sizeof(serv_ip));
- inet_ntop(AF_INET, &guest.sin_addr, guest_ip, sizeof(guest_ip));
- printf("host %s:%d guest %s:%d\n", serv_ip, ntohs(serv.sin_port), guest_ip, ntohs(guest.sin_port));
- n = recv(connfd, buff, MAXLEN,0);
- buff[n] = '\0';
- printf("recv msg from client: %s\n", buff);
3.3.3 AF_INET UDP傳輸最簡單版本
udp_client.c
- /**
- * @file: udpclient.c
- * @brief: A simple Udp server
- * @author: ToakMa <[email protected]>
- * @date: 2014/10/09
- */
- #include <strings.h>
- #include <arpa/inet.h>
- #define PORT 9988
- int main(int argc, char *argv[])
- struct sockaddr_in remote_addr;
- char buff[BUFF_SIZE];
- //1. create a socket
- sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- if (-1 == sockfd)
- perror("udp client socket: ");
- return -1;
- //2. prepare ip and port
- memset(&remote_addr, 0, sizeof(remote_addr));
- remote_addr.sin_family = AF_INET;
- remote_addr.sin_port = htons(PORT);
- remote_addr.sin_addr.s_addr = inet_addr(argv[1]);
- bzero(&(remote_addr.sin_zero), 8);
- //3. sendto
- strcpy(buff, "this a test\n");
- printf("sending : %s\n", buff);
- len = sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr *)&remote_addr, sizeof(remote_addr));
- if (len < 0)
- perror("udp client sendto :");
- //4. close
- return 0;
udp_server.c
- * @file: udpserver.c
- #define PORT 9988
- int sin_len;
- struct sockaddr_in saddr;
- char buff[BUFF_SIZE];
- int res, len;
- //1. create socket
- perror("Udp server socket: ");
- printf("Udp server socket create succ!\n");
- //2. prepare IP and port
- memset(&saddr, 0, sizeof(saddr));
- saddr.sin_family = AF_INET;
- saddr.sin_port = htons(PORT);
- saddr.sin_addr.s_addr = INADDR_ANY;
- bzero(saddr.sin_zero, 8);
- //3. bind
- res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
- if (-1 == res)
- perror("udp server bind: ");
- //4. recvfrom
- printf("Wait for a packet ...\n");
- sin_len = sizeof(struct sockaddr_in);
- len = recvfrom(sockfd, buff, BUFF_SIZE, 0, (struct sockaddr *)&remote_addr, &sin_len);
- if (-1 == len)
- perror("udp server recvform: ");
- buff[len] = '\0';
- printf("Recived packet from %s, contents is: %s \n", \
- inet_ntoa(remote_addr.sin_addr), buff);
- //5. close
3.3.4 AF_INET UNIX本地傳輸最簡單版本
unix_client.c
- #include <sys/un.h>
- #include <errno.h>
- int main()
- int result;
- char ch = 'A';
- //建立socket
- if((sockfd = socket(AF_UNIX,SOCK_STREAM,0) == -1){
- struct sockaddr_un address;
- address.sun_family = AF_UNIX;
- strcpy(address.sun_path,"server_socket");
- int len = sizeof(address);
- //connect()
- if((result = connect(socket,(struct sockaddr*)&address,len)==-1){
- printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
- exit(0);
- //read & write
- write(sockfd,&ch,1);
- read(sockfd,&ch,1);
- printf("char from server = %c\n",ch);
- //close()
- exit(0);
unix_server.c
- int server_len,client_len;
- struct socket_un server_address;
- struct socket_un client_address;
- //删除以前套接字
- unlink("server_socket");
- //socket
- int server_sockfd = socket(AF_UNIX,SOCK_STREAM,0);
- server_address.sun_family = AF_UNIX;
- strcpy(server_address.sun_path,"server_socket");
- server_len = sizeof(server_address);
- //bind()
- bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
- //listen()
- listen(server_sockfd,5);
- char ch;
- printf("server waiting\n");
- client_len = sizeof(client_address);
- client_sockfd = accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);
- //read & write
- read(client_sockfd,&ch,1);
- ch++;
- write(client_sockfd,&ch,1);
- close(client_sockfd);
3.4 網絡程式設計函數
Unix/Linux基本哲學之一就是“一切皆檔案”,都可以用“打開open –> 讀寫write/read –> 關閉close”
- #include<sys/types.h>
- #include<sys/socket.h>
3.4.1 函數socket
建立套接字:socket函數對應于普通檔案的打開操作。普通檔案的打開操作傳回一個檔案描述字,而socket()用于建立一個socket描述符(socket descriptor),它唯一辨別一個socket。
- 函數原型:
- int socket(int domain, int type, int protocol);
- 參數說明:
- domain:即協定域。常用的協定族有,AF_INET(ipv4)、AF_INET6(ipv6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。
- type:指定socket類型。常用的socket類型有,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
- protocol:指定協定。常用的協定有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。
- int:傳回值為-1表示出錯
注意:并不是上面的type和protocol可以随意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的預設協定。
3.4.2 函數bind
命名套接字:bind()函數把一個位址族中的特定位址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6位址和端口号組合賦給socket。
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:是由socket()調用傳回的套接口檔案描述符。
- addr:傳入資料結構sockaddr的指針,包括(IP,protocol,port)需要轉換為通用位址類型struct sockaddr*
- addrlen:以設定成sizeof(struct sockaddr)
- int:傳回值,-1表示出錯
- ipv4位址結構【AF_INET】:
- struct sockaddr_in {
- sa_family_t sin_family; /* address family: AF_INET */
- in_port_t sin_port; /* port in network byte order */
- struct in_addr sin_addr; /* internet address */
- };
- /* Internet address. */
- struct in_addr {
- uint32_t s_addr; /* address in network byte order */
ipv6位址結構【AF_INET6】:
- struct sockaddr_in6 {
- sa_family_t sin6_family; /* AF_INET6 */
- in_port_t sin6_port; /* port number */
- uint32_t sin6_flowinfo; /* IPv6 flow information */
- struct in6_addr sin6_addr; /* IPv6 address */
- uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
- struct in6_addr {
- unsigned char s6_addr[16]; /* IPv6 address */
Unix域位址結構【AF_UNIX】:
- #define UNIX_PATH_MAX 108
- struct sockaddr_un {
- sa_family_t sun_family; /* AF_UNIX */
- char sun_path[UNIX_PATH_MAX]; /* pathname */ 使用strcpy(address.sun_path,"server_socket")
通常伺服器在啟動的時候都會綁定一個衆所周知的位址(如ip位址+端口号),用于提供服務,客戶就可以通過它來接連伺服器;是以伺服器端在listen之前會調用bind()。
而用戶端就不用指定,有系統自動配置設定一個端口号和自身的ip位址組合,在connect()時由系統随機生成一個。由于用戶端不需要固定的端口号,是以不必調用bind(),用戶端的端口号由核心自動配置設定。注意,用戶端不是不允許調用bind(),隻是沒有必要調用
bind()固定一個端口号,伺服器也不是必須調用bind(),但如果伺服器不調用bind(),核心會自動給伺服器配置設定監聽端口,每次啟動伺服器時端口号都不一樣,用戶端要連接配接伺服器就會遇到麻煩。
對server端的addr的初始化如下所示:
- memset(&servaddr, 0, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(5188);
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
首先将整個結構體清零(也可以用bzero函數),然後設定位址類型為AF_INET,網絡位址為INADDR_ANY,這個宏表示本地的任意IP位址,因為伺服器可能有多個網卡,每個網卡也可能綁定多個IP位址,這樣設定可以在所有的IP位址上監聽,直到與某個用戶端建立了連接配接時才确定下來到底用哪個IP位址,端口号為5188。
3.4.3 函數listen
建立套接字隊列
- int listen(int sockfd, int backlog);
- sockfd:即為要監聽的socket描述字
- backlog:相應socket可以排隊的最大連接配接個數
第二個參數是進入隊列中允許的連接配接的個數。進入的連接配接請求在使用系統調用accept()應答之前要在進入隊列中等待。這個值是隊列中最多可以擁有的請求的個數。大多數系統的預設設定為20。你可以設定為5或者10。當出錯時,listen()将會傳回-1值。
這個函數對于backlog的解釋《unix網絡程式設計》P85是說已完成連接配接隊列(ESTABLISHED)和未完成連接配接隊列(SYN_RCVD)之和(即等待連接配接數而非連接配接數)。伺服器調用的accept()傳回并接受這個連接配接,如果有大量的用戶端發起連接配接而伺服器來不及處理,尚未accept的用戶端就處于連接配接等待狀态,listen()聲明sockfd處于監聽狀态,并且最多允許有backlog個用戶端處于連接配接等待狀态,如果接收到更多的連接配接請求就忽略。在計算機早期由于網絡連接配接的處理速度很慢,幾個并發的請求就可能導緻系統處理不過來而引發錯誤,現在這個數字的意義已經不大了,現在軟硬體性能幾乎能保證這個隊列不可能滿。連接配接的狀态變為ESTABLISHED之後(實際是連接配接由從半連接配接隊列轉移到了完成握手的完成隊列)才表示可以被accpet()處理。
3.4.4 函數accept
接受連接配接
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- addr:傳出連接配接客戶的sockaddr指針,包括(IP,protocol,port)
- addrlen:傳入指定客戶結構addr的長度,傳回時該值傳出為連接配接客戶位址addr的實際長度
調用accept()之後,将會傳回一個全新的套接口檔案描述符來處理這個單個的連接配接。這樣,對于同一個連接配接來說,你就有了兩個檔案描述符。原先的一個檔案描述符正在監聽你指定的端口,新的檔案描述符可以用來調用send()和recv()。
可以通過對套接字檔案描述符設定O_NONBLOCK來改變其是否阻塞:
- int flags = fcntl(socket, F_GETFL,0);
- fcntl(socket, F_SETFL, O_NONBLOCK| flags);
3.4.5 函數connect
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用戶端連接配接伺服器,addr為傳入參數,表示目的伺服器的(ip,protocal,port)。
3.4.6 函數read與write
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
- ssize_t read(int fd, void *buf, size_t count);
- ssize_t write(int fd, const void *buf, size_t count);
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
- const struct sockaddr *dest_addr, socklen_t addrlen);
- ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
- struct sockaddr *src_addr, socklen_t *addrlen);
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
3.4.7 函數close
- int close(int fd);
注意:close操作隻是使相應socket描述字的引用計數-1,隻有當引用計數為0的時候,才會觸發TCP用戶端向伺服器發送終止連接配接請求。
3.4.8 函數getsockname
函數傳回與套接口關聯的本地協定位址。
- int getsockname(int sockfd, struct sockaddr * localaddr, socken_t * addrlen);
3.4.9 函數getpeername
函數傳回與套接口關聯的遠端協定位址。
- int getpeername(int sockfd,struct sockaddr* peeraddr,int* addrlen);
- peeraddr:傳出資料結構sockaddr的指針,包括(IP,protocol,port)
- addrlen:傳出結構大小的指針
3.5 網絡位元組序與主機位元組序轉換
将主機位元組序轉換為網絡位元組序(避免大端小端問題)
#include <netinet/in.h>
3.5.1 函數htonl
- uint32_t htonl(uint32_t hostlong);
3.5.2 函數htons
- uint16_t htons(uint16_t hostshort);
3.5.3 函數ntohl
- uint32_t ntohl(uint32_t netlong);
3.5.4 函數ntohs
- uint16_t ntohs(uint16_t netshort);
3.6 IP位址與主機位元組序轉換
3.6.1 函數inet_pton
[将“點分十進制” -> “整數”]
- int inet_pton(int af, const char *src, void *dst);
- af:位址族,AF_INET為ipv4位址,AF_INET6為ipv6位址
- src:為ip位址(ipv4例如1.1.1.1)
- dst:函數将該位址轉換為in_addr的結構體,并複制在*dst中
- int:傳回值:如果函數出錯将傳回一個負值,并将errno設定為EAFNOSUPPORT,如果參數af指定的位址族和src格式不對,函數将傳回0。
3.6.2 函數inet_ntop
[将“整數” -> “點分十進制”]
- const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
3.7 主機資料庫函數
3.7.1 函數gethostname
它傳回(本地)計算機的名字,存儲在hostname中,大小為size
- int gethostname(char*hostname,size_tsize);
- int:傳回值gethostname将傳回0。如果失敗,它将傳回-1。
3.7.2 函數gethostbyname
根據域名或者主機名擷取資訊
- struct hostent *gethostbyname(const char *name);
- name:主機名,如"www.baidu.com"
- hostend:傳回值。如果函數調用失敗,将傳回NULL。
hostend的結構如下:
- struct hostent
- char *h_name; //表示的是主機的規範名,例如www.google.com的規範名其實是www.l.google.com
- char **h_aliases; //表示的是主機的别名
- int h_addrtype; //IP位址的類型
- int h_length; //IP位址的長度
- char **h_addr_list; //主機的ip位址,注意這是以網絡位元組順序儲存的一個值,用inet_ntop恢複
示例代碼:
- #include <netdb.h>
- int main(int argc, char **argv)
- char *host,**names;
- struct hostent *hostinfo;
- cha str[32];
- /* 取得指令後第一個參數,即要解析的域名或主機名 */
- if(argc==1){
- char myname[256];
- gethostname(myname,255);
- host=myname;
- else{
- host=argv[1];
- /* 調用gethostbyname()。調用結果都存在hostinfo中 */
- if( (hostinfo = gethostbyname(host) ) == NULL )
- printf("gethostbyname error for host:%s/n", host);
- exit(1); /* 如果調用gethostbyname發生錯誤,傳回1 */
- /* 将主機的規範名打出來 */
- printf("official hostname:%s/n",hostinfo->h_name);
- /* 主機可能有多個别名,将所有别名分别打出來 */
- for(names = hostinfo->h_aliases; *names != NULL; names++)
- printf(" alias:%s/n",*names);
- /* 根據位址類型,将位址打出來 */
- switch(hostinfo->h_addrtype)
- case AF_INET:
- case AF_INET6:
- names=hostinfo->h_addr_list;
- /* 将剛才得到的所有位址都打出來。其中調用了inet_ntop()函數 */
- for(;*names!=NULL;names++)
- printf(" address:%s/n", inet_ntop(hostinfo->h_addrtype, *names, str, sizeof(str)));
- break;
- default:
- printf("unknown address type/n");
- }
3.7.3 函數gethostbyaddr
根據ip位址(網絡位元組序)擷取資訊
- struct hostent *gethostbyaddr(const char *name,int len,int type)
- name:ip位址,例如: inet_addr("192.168.4.111")
- len:
- type:
3.7.4 函數getservbyname
用于根據給定的名字來查找相應的伺服器,傳回對應于給定服務名和協定名的相關服務資訊
- struct servernt *gerservbyname(const char *servname,const char *protoname)
- servname:例如smtp
- protoname:例如tcp
servent結構如下:
- struct servent{
- char *s_name; /*服務的正規名字*/
- char **s_aliases;/*别名清單*/
- int s_port; /*服務的端口号*/
- char *s_proto; /*服務的協定,如tcp或udp*/
3.7.5 函數getservbyport
傳回與給定服務名對應的包含名字和服務号資訊的servent結構指針(注意參數port的值必須是網絡位元組序類型的,是以在使用的時候需要使用函數htons(port)來進行轉換)。
- struct servent *getservbyport(int port,const char *proroname);
- 函數示例:
- sptr=getservbyport(htons(53),"udp");
- #擷取任意已知主機的日期和時間
- char *host;
- struct hostent * hostinfo;
- struct servent * servinfo;
- host="localhost";
- if ( (servinfo = getservbyname("daytime","tcp")) );
- printf("no daytime service/n");
- exit(1); /* 如果調用getservbyname發生錯誤,傳回1 */
- /* 将daytime的端口列印出來*/
- printf("daytime port is %d/n",ntohs(servinfo -> s_port));
- int sockfd = socket(AF_INET, SOCK_STREAM,0);
- struct sockaddr_in address;
- address.sin_family = AF_INET;
- address.sin_port = servinfo -> s_port;
- address.sin_addr= *(struct in_addr*)*hostinfo->h_addr_list;
- if((result = connect(socket,(struct sockaddr*)&address,len))==-1){
- exit(0);
- char buffer[128];
- result = read(sockfd,buffer,sizeof(buffer));
- buffer[result]='\0';
- printf("read %d bytes: %s",result,buffer);
3.5 多客戶程式設計
3.5.1 阻塞與非阻塞
(1)阻塞block
所謂阻塞方式block,顧名思義,就是程序或是線程執行到這些函數時必須等待某個事件的發生,如果事件沒有發生,程序或線程就被阻塞(程序Sleep),函數不能立即傳回。
例如socket程式設計中connect、accept、recv、recvfrom這樣的阻塞程式。
再如絕大多數的函數調用、語句執行,嚴格來說,他們都是以阻塞方式執行的。
(2)非阻塞non-block
所謂非阻塞方式non-block,就是程序或線程執行此函數時不必非要等待事件的發生,一旦執行肯定傳回,以傳回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則傳回一個代碼來告知事件未發生,而程序或線程繼續執行,是以效率較高。
非阻塞I/O有一個缺點,如果所有裝置都一直沒有資料到達,調用者需要反複查詢做無用功,如果阻塞在那裡,作業系統可以排程别的程序執行,就不會做無用功了。?????
3.5.2 函數select
- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:為檔案描述符集合中的最大值+1,避免函數檢查fd_set的所有1024位
- readfds:被監控是否可以讀
- writefds:被監控是否可以寫
- exceptfds:被監控是否發生異常
- timeout:逾時時間,Linux傳回時會被修改成剩餘的時間。
- int:傳回值,傳回狀态發生變化的描述符的總數,錯誤傳回-1,逾時傳回0。
關于timeout:
- struct timeval結構如下:
- struct timeval
- long tv_sec; //seconds
- long tv_usec; //microseconds
1,若timeout設定為t>0,表示等待固定時間,有一個fd位被置為1或者時間耗盡,函數均傳回。
2,若timeout設定為t=0,表示非阻塞,函數檢查完每個fd後立即傳回。
3,若timeout設定為t="NULL",表示阻塞,直到有一個fd位被置為1函數才傳回。
- 相關操作:
- FD_ZERO(fd_set *set); //fd_set很多系統實作為bit arrays,将所有的檔案描述符從fd_set中清空
- FD_CLR(int fd, fd_set *set); //從set中清除fd
- FD_SET(int fd, fd_set *set); //将fd添加到set中
- FD_ISSET(int fd, fd_set *set);//判斷描述符fd是否在給定的描述符集set中,通常配合select函數使用,由于select函數成功傳回時會将未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函數傳回後,某個描述符是否準備好,以便進行接下來的處理操作。
常用代碼:
- fd_set rdfds;
- struct timeval tv;
- tv.tv_sec = 1;
- tv.tv_uses = 500;
- int ret;
- FD_ZERO(&rdfds);
- FD_SET(socket, &rdfds);
- ret = select (socket + 1, %rdfds, NULL, NULL, &tv);
- if(ret < 0)
- perror ("select");
- else if (ret == 0)
- printf("time out");
- else {
- printf(“ret = %d/n”,ret);
- if(FD_ISSET(socket, &rdfds)){
- /* 讀取socket句柄裡的資料 */
- recv( );
- }
3.5.3 函數pselect
- #include <sys/select.h>
- int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, const struct timespec *restrict tsptr,const sigset_t *restrict sigmask);
- 參數說明
- 傳回值:Returns: count of ready descriptors, 0 on timeout, -1 on error
它與select的差別在于:
1,pselect使用timespec結構指定逾時值。timespec結構以秒和納秒表示時間,而非秒和微秒。
2,pselect的逾時值被聲明為const,這保證了調用pselect不會改變timespec結構。
3,pselect可使用一個可選擇的信号屏蔽字。在調用pselect時,以原子操作的方式安裝該信号屏蔽字,在傳回時恢複以前的信号屏蔽字。
3.5.4 函數poll
- #include <sys/poll.h>
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
- timeout:【值=INFTIM】表示永遠等待【值=0】表示立即傳回,不阻塞程序【值>0】等待指定數目的毫秒數。
pollfd結構說明:
- struct pollfd {
- int fd; /* 檔案描述符 */
- short events; /* 等待的事件 */
- short revents; /* 發生的事件 */
- poll函數可用的測試值:
- 常量 說明
- POLLIN 普通或優先級帶資料可讀
- POLLRDNORM 普通資料可讀
- POLLRDBAND 優先級帶資料可讀
- POLLPRI 高優先級資料可讀
- POLLOUT 普通資料可寫
- POLLWRNORM 普通資料可寫
- POLLWRBAND 優先級帶資料可寫
- POLLERR 發生錯誤
- POLLHUP 發生挂起
- POLLNVAL 描述字不是一個打開的檔案
- 注意:後三個隻能作為描述字的傳回結果存儲在revents中,而不能作為測試條件用于events中。
3.5.5 函數epoll
一共有三個函數:
- int epoll_create(int size);
建立一個epoll的句柄,size用來告訴核心這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll句柄後,它就是會占用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數,它不同與select()是在監聽事件時告訴核心要監聽什麼類型的事件,而是在這裡先注冊要監聽的事件類型。第一個參數是epoll_create()的傳回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中删除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴核心需要監聽什麼事。
struct epoll_event結構如下:
- struct epoll_event {
- __uint32_t events; /* Epoll events */
- epoll_data_t data; /* User data variable */
events可以是以下幾個宏的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被挂斷;
EPOLLET: 将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的。
EPOLLONESHOT:隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的産生,類似于select()調用。參數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個 maxevents的值不能大于建立epoll_create()時的size,參數timeout是逾時時間(毫秒,0會立即傳回,-1将不确定,也有 說法說是永久阻塞)。該函數傳回需要處理的事件數目,如傳回0表示已逾時。
在前面講過的客戶/伺服器程式中,伺服器隻能處理一個用戶端的請求,如何同時服務多個用戶端呢?在未講到select/poll/epoll等進階IO之前,比較老土的辦法是使用fork來實作。網絡伺服器通常用fork來同時服務多個用戶端,父程序專門負責監聽端口,每次accept一個新的用戶端連接配接就fork出一個子程序專門服務這個用戶端。但是子程序退出時會産生僵屍程序,父程序要注意處理SIGCHLD信号和調用wait清理僵屍程序,最簡單的辦法就是直接忽略SIGCHLD信号。