使用TCP協議的socket服務器
1.網絡字節序網絡
因爲在主機存儲爲小端序,網絡傳輸爲大端序,而且在網絡中須要讀取IP號和端口號,因此發送端要將小端序轉爲大端序,接收端將大端序轉爲小端序併發
#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);dom
表示host,n表示network,l表示32位長整數,s表示16位短整數。socket
2.IP地址轉換函數ide
#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);函數
3.構造一個sockaddrui
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本地任意IP(因爲網卡可能綁定了多個IP)
servaddr.sin_port = htons(8000);
spa
這樣設置能夠在全部的IP地址上監聽,直到與某個客戶端創建了鏈接時才肯定下來到底用哪一個IP地址,端口號爲8000。命令行
4.socket函數
int socket(int domain, int type, int protocol);
參數: domain:1. AF_INET (ipv4) 2.AF_INET6 (ipv6)
type: 1. SOCK_STREAM (數據流) 2.SOCK_DGRAM(數據報) 3.SOCK_RAW(ICMP使用)
protocol: 0 默認協議
返回值:成功返回一個新的文件描述符,失敗返回-1,設置errno
5.綁定函數:bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
將構造的sockaddr和建立的socket綁定在一塊兒。
參數:
1.sockfd:socket文件描述符
2.addr:構造出IP地址加端口號
3.addrlen:sizeof(addr)長度
返回值:成功返回0,失敗返回-1, 設置errno
服務器程序所監聽的網絡地址和端口號一般是固定不變的,客戶端程序得知服務器程序的地址和端口號後就能夠向服務器發起鏈接,所以服務器須要調用bind綁定一個固定的網絡地址和端口號。
6.監聽函數:listen
int listen(int sockfd, int backlog);
參數:1.sockfd:socket文件描述符
2.backlog:排隊創建3次握手隊列和剛剛創建3次握手隊列的連接數和
返回值:listen()成功返回0,失敗返回-1。
7.accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數: 1.sockdf:socket文件描述符
2.addr:傳出參數,返回連接客戶端地址信息,含IP地址和端口號
3.addrlen:傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小
返回值:成功返回一個新的socket文件描述符,用於和客戶端通訊,失敗返回-1,設置errno
三方握手完成後,服務器調用accept()接受鏈接
accept()的參數listenfd是先前的監聽文件描述符,而accept()的返回值是另一個文件描述符connfd,以後與客戶端之間就經過這個connfd通信,最後關閉connfd斷開鏈接,而不關閉listenfd,再次回到循環開頭listenfd仍然用做accept的參數。accept()成功返回一個文件描述符,出錯返回-1。
8.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。
C/S模型-TCP
服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
數據傳輸的過程:創建鏈接後,TCP協議提供全雙工的通訊服務,可是通常的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。所以,服務器從accept()返回後馬上調用read(),讀socket就像讀管道同樣,若是沒有數據到達就阻塞等待,這時客11.3節C/S模型-TCP 123
戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一
條請求,如此循環下去。若是客戶端沒有更多的請求了,就調用close()關閉鏈接,就像寫端關閉的管道同樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了鏈接,也調用close()關閉鏈接。
注意,任何一方調用close()後,鏈接的兩個傳輸方向都關閉,不能再發送數據了。若是一方調用shutdown()則鏈接處於半關閉狀態,仍可接收對方發來的數據。
能夠看出在TCP創建鏈接後發送數據:能夠用send和recv,也能夠用read和write。
實例:server.c的做用是從客戶端讀字符,而後將每一個字符轉換爲大寫並回送給客戶端。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; //建立socket listenfd = socket(AF_INET, SOCK_STREAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); //將socket和sockaddr綁定 bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //監聽 listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr);//每次都要更新 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); write(connfd, buf, n); close(connfd); } }
client.c的做用是從命令行參數中得到一個字符串發給服務器,而後接收服務器返回的
/* client.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; char *str; if (argc != 2) { fputs("usage: ./client message\n", stderr); exit(1); } str = argv[1]; //創建socket sockfd = socket(AF_INET, SOCK_STREAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);//將十進制ip地址轉化 servaddr.sin_port = htons(SERV_PORT); //發起鏈接 connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //向服務器發送數據 write(sockfd, str, strlen(str)); //收數據 n = read(sockfd, buf, MAXLINE); printf("Response from server:\n"); //打印到輸出設備 write(STDOUT_FILENO, buf, n); //關閉套接字 close(sockfd); return 0; }
注意:recv和send函數 與 read和write函數相似,只多了一個參數flag 通常默認爲0.
因爲客戶端不須要固定的端口號,所以沒必要調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不容許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但若是服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啓動服務器時端口號都不同,客戶端要鏈接服務器就會遇到麻煩。
C/S模型-UDP
服務器端:
/* server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int sockfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; //建立socket sockfd = Socket(AF_INET, SOCK_DGRAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); //將socket與sockaddr綁定 Bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); //接收來自客戶端的數據 n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len); if (n == -1) perr_exit("recvfrom error"); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //向服務器端發送數據 n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); if (n == -1) perr_exit("sendto error"); } }
客戶端:
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; int sockfd, n; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; socklen_t servaddr_len; //建立socket sockfd = Socket(AF_INET, SOCK_DGRAM, 0); //設置sockaddr bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); while (fgets(buf, MAXLINE, stdin) != NULL) { //向服務器端發送數據 n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); if (n == -1) perr_exit("sendto error"); //從客戶端接收數據 n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0); if (n == -1) perr_exit("recvfrom error"); //顯示到屏幕上 Write(STDOUT_FILENO, buf, n); } //關閉套接字 Close(sockfd); return 0; }
recvfrom和sendto 的第五個參數爲傳出參數,記錄發來的數據源sockaddr,服務器根據這個參數記錄客戶端的socket信息,併發送數據。