1. 程式人生 > >Linux__網路程式設計套接字(UDP/TCP)

Linux__網路程式設計套接字(UDP/TCP)

重點知識:          ·IP地址、埠號、網路位元組序的基本概念          ·socket api的基本用法          ·實現簡單的UDP客戶端/伺服器          ·實現簡單的TCP客戶端/伺服器(單鏈接版本,多程序版本,多執行緒版本)          ·理解tcp伺服器建立連線,傳送資料,斷開連線的流程基礎概念:     IP地址:IP協議有兩個版本,IPV4和IPV6,但現在我們使用的是IPV4,IPV6還沒有普及,所以我們這裡談的都是IPV4.     ·IP地址是在IP協議中,用來標識網路中不同主機的地址。     ·IPV4的IP地址是一個4位元組,32位的整數     ·通常我們更習慣使用點分十進位制的字串來表示IP地址,例如 192.168.0.1;點分十進位制的每一個數字表示一個位元組,範圍是0~255;源IP地址和目的IP地址
     在IP資料報頭部中,有兩個IP地址,分別表示源IP地址和目的IP地址(源IP地址即傳送方的IP,目的地址即要到達的主機IP地址)埠號     事實上,光有IP地址是沒辦法通訊的,比如我們通過IP得知找到了主機,可是要把報文交給諸多程序中的哪一個呢,我們還需要其他的東西來標識主機上的程序,這就是埠號,埠號是唯一標示一臺主機上的一個程序,這樣資料報到了主機,就能找到接收報文的程序。     ·埠號是傳輸層協議的內容,是一個2位元組16位的整數;     ·埠號用來標識一個程序,告訴作業系統,當前的資料報文要交給哪一個程序來處理;     ·IP地址+埠號的組合就可以標識網路上的某一臺主機上的某一個程序;     ·一個埠號只能被一個程序佔用;     ·一個程序可以繫結多個埠號,但是一個埠號是不能被多個程序繫結的。     ·一個網路程式對應一個埠——》一個埠對應一至多個程序—》一個程序對應一至多個執行緒。源埠號和目的埠號
傳輸層協議(TCP/UDP)的資料段中有兩個埠號,分別叫做源埠號和目的埠號,就是描述“資料是誰發的,資料是發給誰的”這個規則。TCP/UDP初步認識     TCP:          ·傳輸層協議          ·有連線          ·可靠傳輸          ·面向位元組流     UDP:          ·傳輸層協議          ·無連線          ·不可靠傳輸          ·面向資料報網路位元組序     在學習C語言的時候瞭解過一個知識點,機器打大端小端,實際是記憶體中的位元組資料相對於記憶體地址有大端小端之分,而磁碟中的位元組資料相對於檔案中的偏移地址也有大端小端之分,其實網路資料流也同樣有大端小端之分。     ·傳送主機通常將傳送緩衝區中的資料按照記憶體地址從低到高的順序發出     ·接收主機把從網路上收到的資料依次儲存在緩衝區裡,也是按記憶體地址從低到高的順序儲存     ·因此網路資料流的地址規定:先發出的資料是低地址,後發出的資料是高地址     ·TCP/IP協議規定,網路資料流應該採用大端字序,即低地址高位元組     ·不論這臺主機是大端機還是小端機,都會按照這個TCP/IP規定的網路位元組序來發送/接收資料     ·如果當前機器是小斷,需要轉成大端,否則就忽略,直接傳送這裡需要用到以下函式實現網路位元組序和主機位元組序的轉換,使得網路程式具有可移植性,同樣的程式碼能在大端機和小端機上都正常執行:
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t htonshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
     ·h表示host(主機),n表示network(網路),l表示32位長整數,s表示16位短整數     ·例如htons表示將16位的短整數從主機序轉換為網路位元組序。例如轉換埠號     ·如果主機是小端位元組序,這些引數會做相應的轉換然後返回     ·如果主機是大端位元組序,這些函式什麼也不會做,將引數原原本本的返回socket程式設計socket常見API
//建立socket檔案描述符(TCP/UDP,客戶端+伺服器)int sock(int domain,int type,int protocol);//繫結埠號(TCP/UDP,伺服器)int bind(int socket,const struct sockaddr *address,socklen_t address_len);//開始監聽socket(TCP,伺服器)int listen(int socket,int backlog);//接受請求(TCP,伺服器)int accept(int socket,struct sockaddr* address,socklen_t* address_len);//建立連線(TCP,客戶端)int connect(int socket,const struct sockaddr* addr,socklen_t addrlen);
sockaddr結構     socket API是一層抽象的網路程式設計介面,適用於各種底層網路協議,如IPV4,IPV6,以及UNIX Domain Socket等,然而各種網路協議的地址格式是不同的。·IPV4和IPV6的地址格式定義在netinet/in.h中,IPV4使用了sockaddr_in結構體表示,包括16位地址型別,16位埠號,32位ip地址·IPV4和IPV6地址型別分別定義為常數AF_INET,AF_INET6,這樣,只需要取得sockaddr結構體的首地址,而並不需要直到具體是哪種型別的sockaddr結構體,就可以根據地址型別欄位確定結構體中的內容。·socket API可以都用struct sockaddr*來表示,在使用的時候需要強制轉化為sockaddr_in;這樣的好處是程式的通用性,可以接收IPV4,IPV6,以及UNIX Domain Socket各種型別的sockaddr結構體指標作為引數;·雖然socket api的介面是sockaddr,但是我們真正在基於IPV4程式設計時,使用的資料結構是sockaddr_in;這個結構裡主要有三部分資訊:地質型別,埠號,IP地址。網路伺服器編寫UDP伺服器
#include <stdio.h>#include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdlib.h>#include <string.h>static void usage(const char* msg){printf("Usage: [%s]\n”,msg,);}int main(int argc,char* argv[]){if(argc != 3){usage(argv[0]);return 1;}int sock = socket(AF_INET,SOCK_DGRAM,0);//建立套接字if(sock < 0){perror("socket");return 2;}struct sockaddr_in server;//建立server端的sockaddr結構體server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));/* server.sin_addr.s_addr = htonl(INADDR_ANY); */server.sin_addr.s_addr = inet_addr(argv[1]);if(bind(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//繫結埠號perror("bind");return 3;}char buf[1024];/* memset(buf,'\0',sizeof(buf)); */struct sockaddr_in client;for(;;){socklen_t len = sizeof(client);ssize_t s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);、、接收訊息printf("%lu\n",s);if(s > 0){buf[s] = '0';printf("[%s|%d]: %s\n",inet_ntoa(client.sin_addr),htons(client.sin_port),buf);sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//傳送回去}}return 0;}
UDP客戶端
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>void usage(const char* msg){printf("Usage: %s\n",msg);}int main(int argc,char* argv[]){if(argc != 3){usage(argv[0]);return 1;}int sock = socket(AF_INET,SOCK_DGRAM,0);if(sock < 0){perror("socket");return 2;}struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));/* server.sin_addr.s_addr = htonl(INADDR_ANY); */server.sin_addr.s_addr = inet_addr(argv[1]);char buf[1024];memset(buf,'\0',sizeof(buf));struct sockaddr_in local;for(;;){socklen_t len = sizeof(local);printf("Please Enter#: ");fflush(stdout);ssize_t s = read(0,buf,sizeof(buf)-1);if(s > 0){buf[s-1] = '\0';//去回車sendto(sock,&buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));//向伺服器傳送訊息ssize_t _s = recvfrom(sock,&buf,sizeof(buf),0,(struct sockaddr*)&local,&len);//接受伺服器返回的訊息if(_s > 0){printf("Server echo#:%s\n",buf);}}}}
地址轉換函式     ·字串轉in_addr的函式
#include <arpa/inet.h>int inet_aton(const char* strptr,struct in_addrptr);in_addr_t inet_addr(const char *strptr);int inet_pton(int family,const char* strptr,void* addrptr);
     ·in_addr轉字串的函式
char* inet_ntoa(struct in_addr inaddr);const char* inet_ntop(int family,const char* addrptr,char *strptr,size_t len);
     其中inet_pton和inet_ntop不僅可以轉換IPV4的in_addr,還可以轉換IPV6_addr,因此函式介面是void* addrptr.簡單的TCP網路程式     TCP伺服器
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>int startup(char* ip,int port){int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){perror("socket");exit(1);}int opt = 1;//地址複用,避免time_wait導致無法使用setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip);if(bind(sock,(struct sockaddr*)&server,sizeof(server)) < 0){perror("bind");exit(3);}if(listen(sock,5) < 0){perror("listen");exit(4);}return sock;}int main(int argc,char* argv[]){if(argc != 3){printf("Usage: [%s] [ip] [port]\n",argv[0]);return 1;}int listen_sock = startup(argv[1],atoi(argv[2]));struct sockaddr_in client;socklen_t len = sizeof(client);for(;;){int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);if(new_sock < 0){perror("accept");continue;}char buf_ip[1024];memset(buf_ip,'\0',sizeof(buf_ip));inet_ntop(AF_INET,&client.sin_addr,buf_ip,sizeof(buf_ip));printf("get a new connect:[%s|%d]\n",buf_ip,ntohs(client.sin_port));while(1){char buf[1024];memset(buf,'\0',sizeof(buf));read(new_sock,buf,sizeof(buf));printf("client: %s\n",buf);printf("server :$ ");fflush(stdout);memset(buf,'\0',sizeof(buf));ssize_t s = read(0,buf,sizeof(buf)-1);buf[s-1] = '\0';write(new_sock,buf,sizeof(buf));printf("Please wait:...");}}close(listen_sock);return 0;}
TCP客戶端
clude <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/socket.h>#include <sys/types.h>#include <string.h>#include <netinet/in.h>#include <arpa/inet.h>int main(int argc,char* argv[]){if(argc != 3){printf("Usage: %s ip port\n",argv[0]);}char buf[1024];memset(buf,'\0',sizeof(buf));struct sockaddr_in server;int sock = socket(AF_INET,SOCK_STREAM,0);bzero(&server,sizeof(server));server.sin_family = AF_INET;inet_pton(AF_INET,"127.0.0.1",&server.sin_addr);server.sin_port = htons(atoi(argv[2]));int ret = connect(sock,(struct sockaddr*)&server,sizeof(server));if(ret < 0){perror("connect");return 2;}printf("connect success.....\n");while(1){printf("client:# ");fflush(stdout);ssize_t s = read(0,buf,sizeof(buf)-1);if(s > 0){buf[s] = '\0';write(sock,buf,strlen(buf));memset(buf,'\0',sizeof(buf));read(sock,buf,sizeof(buf));printf("server: %s\n",buf);}}close(sock);return 0;}
     由於客戶端不需要固定的埠號,所以不用呼叫bind(),客戶端的埠號可以指定分配。但也不是客戶端不允許使用bind();而是沒有必要和固定的埠號綁在一起,否則如果在一臺機器上啟動多個客戶端,就會出現埠號被佔用的情況而無法正常建立連線。     伺服器也不是一定要呼叫bind();但如果伺服器不呼叫bind();核心就會自動給伺服器分配監聽埠,每次啟動伺服器的埠號都不一樣,客戶端需要連結伺服器是就會比較麻煩。