2 C/S模型-TCP
大部分進程間通信使用客戶端-服務器模型。進程間通信指的是兩個進程之間相互通信,其中,客戶端進程連接服務器進程,通常是發出數據請求。一個很好的比喻是一個人給另一個人打電話,打出電話的人就好比客戶端,接電話的人就好比服務器。
有兩點需要注意,第一,客戶端需要知道服務器是否存在,如果服務器存在,服務器的地址是多少;但是在客戶端連接服務器之前,服務器並不需要知道客戶端的地址(甚至客戶端存在與否)。第二,連接一旦建立,客戶端和服務器之間就可以接收和發送數據。
客戶端和服務器用於建立連接的系統調用有所不同,但都包含socket的基本構建。
客戶端建立socket的步驟:
- 使用socket()函數(系統調用)創建一個socket
- 使用connect()函數將socket連接到服務器的地址
- 發送或接收數據。有很多種方法進行發動數據和接收數據,但是最簡單的方法是使用read()和write()函數
服務器端建立socket的步驟:
- 使用socket()函數創建一個socket
- 使用bind()函數將socket和一個地址綁定,對於網絡上的服務器來講,地址包含主機和端口號,像這樣:127.0.0.1:8888
- 使用listen()函數監聽連接
- 當監聽到有連接時,使用accept()函數接受一個連接。這個函數通常會堵塞,直到客戶端連接建立。
- 接收和發送數據
服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
數據傳輸的過程:
1) 建立連接後,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段。
實例<客戶端/服務器>
mkdir server_test touch server.c touch client.c touch Makefile |
server.c |
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h>
#define SERVER_PORT 8000 #define MAXLINE 4096
int main(void) { struct sockaddr_in serveraddr, clientaddr; int sockfd, addrlen, confd, len, i; char ipstr[128]; char buf[MAXLINE];
//1.socket sockfd = socket(AF_INET, SOCK_STREAM, 0); //2.bind 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 listen(sockfd, 128); while (1) { //4.accept阻塞監聽客戶端鏈接請求 addrlen = sizeof(clientaddr); confd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);//返回的是客戶端和服務端專用通道的socket描述符 //輸出客戶端IP地址和端口號 inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr)); 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.處理客戶端請求 len = read(confd, buf, sizeof(buf)); i = 0; while (i < len) { buf[i] = toupper(buf[i]); i++; } write(confd, buf, len);
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[] = "127.0.0.1"; 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.254" 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);
//5.關閉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 |
由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啓動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。客戶端和服務器啓動後可以查看鏈接情況:netstat-apn|grep 8000
3 C/S模型-UDP
實例<客戶端/服務器>
udp_server.cpp |
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <stdio.h> #include <string.h>
using namespace std;
#define BUFFER_SIZE 1024
void tsocket(int argc, const char * argv[]);
int main(int argc, const char * argv[]) { tsocket(argc,argv); return 0; } void tsocket(int argc, const char * argv[]){ if(argc < 3){ exit(-1); }
const char* ip = argv[1]; int port = atoi(argv[2]); int backlog = atoi(argv[3]);
std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog << std::endl;
int fd; int check_ret;
fd = socket(PF_INET,SOCK_DGRAM , 0); assert(fd >= 0);
struct sockaddr_in address; bzero(&address,sizeof(address));
//轉換成網絡地址 address.sin_port = htons(port); address.sin_family = AF_INET; //地址轉換 inet_pton(AF_INET, ip, &address.sin_addr);
//綁定ip和端口 check_ret = bind(fd,(struct sockaddr*)&address,sizeof(address)); assert(check_ret >= 0);
while(1){ char buffer[BUFFER_SIZE]; struct sockaddr_in addressClient; socklen_t clientLen = sizeof(addressClient); memset(buffer, '\0', BUFFER_SIZE); //獲取信息 if(recvfrom(fd, buffer, BUFFER_SIZE-1,0,(struct sockaddr*)&addressClient, &clientLen) == -1) { perror("Receive Data Failed:"); exit(1); } printf("buffer=%s\n", buffer); } close(fd); } |
udp_client.cpp |
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <stdio.h> #include <string.h>
using namespace std;
void tserver(int argc, const char * argv[]);
int main(int argc, const char * argv[]) { tserver(argc,argv); return 0; } void tserver(int argc, const char * argv[]){ std::cout << "t server" << std::endl; if(argc < 3){ exit(-1); }
const char* ip = argv[1]; int port = atoi(argv[2]); int backlog = atoi(argv[3]);
std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog << std::endl;
int fd; int check_ret;
fd = socket(PF_INET,SOCK_DGRAM , 0); assert(fd >= 0);
struct sockaddr_in addressServer; bzero(&addressServer,sizeof(addressServer));
//轉換成網絡地址 addressServer.sin_port = htons(port); addressServer.sin_family = AF_INET; //地址轉換 inet_pton(AF_INET, ip, &addressServer.sin_addr); //發送數據 const char* normal_data = "my boy!"; if(sendto(fd, normal_data, strlen(normal_data),0,(struct sockaddr*)&addressServer,sizeof(addressServer)) < 0) { perror("Send File Name Failed:"); exit(1); } close(fd); } |
4 select模型
select系統調用時用來讓我們的程序監視多個文件句柄的狀態變化的。程序會停在select這裏等待,直到被監視的文件句柄有一個或多個發生了狀態改變。
文件句柄,其實就是一個整數,表示一個系統資源,通過socket函數的聲明就明白了:
int socket(int domain, int type, int protocol);
我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE*結構的表示就是stdin、stdout、stderr。
繼續上面的select,就是用來監視某個或某些句柄的狀態變化的。select函數原型如下:
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
函數的最後一個參數timeout是一個超時時間值。其類型是struct timeval *,即一個struct timeval結構的變量的指針,所以我們在程序裏要聲明一個struct timeval tv;然後把變量tv的地址&tv傳遞給select函數。struct timeval結構如下:
struct timeval { long tv_sec; //seconds long tv_usec; //microseconds }; |
第2、3、4三個參數是一樣的類型fd_set *,即我們在程序裏要申請幾個fd_set類型的變量,比如rdfds,wtfds,exfds,然後把這個變量的地址&rdfds,&wtfds,&exfds傳遞給select函數。這三個參數都是一個句柄的集合,第一個rdfds是用來保存這樣的句柄的:當句柄的狀態變成可讀時系統就告訴select函數返回,同理第二個函數是指向有句柄狀態變成可寫時系統就會告訴select函數返回,同理第三個參數exfds是特殊情況,即句柄上有特殊情況發生時系統會告訴select函數返回。特殊情況比如對方通過一個socket句柄發來了緊急數據。如果我們程序裏只想檢測某個socket是否有數據可讀,我們可以這樣:
fd_set rdfds; struct timeval tv; int ret; FD_ZERO(&rdfds); FD_SET(socket, &rdfds); tv.tv_sec = 1; tv.tv_uses = 500; 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(...); } } |
注意select函數的第一個參數,是所有加入集合的句柄值的最大那個那個值還要加1.比如我們創建了3個句柄;
int sa, sb, sc; sa = socket(……); connect (sa,….);
sb = socket(….); connect (sb,…);
sc = socket(….); connect(sc,…);
FD_SET(sa, &rdfds); FD_SET(sb, &rdfds); FD_SET(sc, &rdfds); |
在使用select函數之前,一定要找到3個句柄中的最大值是哪個,我們一般定義一個變量來保存最大值,取得最大socket值如下:
int maxfd = 0; if(sa > maxfd) maxfd = sa; if(sb > maxfd) maxfd = sb; if(sc > maxfd) maxfd = sc; |
然後調用select函數:
ret = select (maxfd+1, &rdfds, NULL, NULL,&tv); |
同樣的道理,如果我們是檢測用戶是否按了鍵盤進行輸入,我們就應該把標準輸入0這個句柄放到select裏來檢測,如下:
FD_ZERO(&rdfds); FD_SET(0, &rdfds); tv.tv_sec = 1; tv.tv_usec = 0; ret = select (1, &rdfds,NULL,NULL,&tv); if(ret < 0) perror(“select”); else if (ret = = 0) printf (“time out/n”); else{ scanf(“%s”,buf); } |
實例<服務器>
使用select函數可以以非阻塞的方式和多個socket通信。程序只是演示select函數的使用,功能非常簡單,即使某個連接關閉以後也不會修改當前連接數,連接數達到最大值後會終止程序。
1. 程序使用了一個數組fd_A,通信開始後把需要通信的多個socket描述符都放入此數組。
2. 首先生成一個叫sock_fd的socket描述符,用於監聽端口。
3. 將sock_fd和數組fd_A中不爲0的描述符放入select將檢查的集合fdsr。
4. 處理fdsr中可以接收數據的連接。如果是sock_fd,表明有新連接加入,將新加入連接的socket描述符放置到fd_A。
select_server.cpp |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
#define MYPORT 1234 // the port users will be connecting to
#define BACKLOG 5 // how many pending connections queue will hold
#define BUF_SIZE 200
int fd_A[BACKLOG]; // accepted connection fd int conn_amount; // current connection amount
void showclient() { int i; printf("client amount: %d\n", conn_amount); for (i = 0; i < BACKLOG; i++) { printf("[%d]:%d ", i, fd_A[i]); } printf("\n\n"); }
int main(void) { int sock_fd, new_fd; // listen on sock_fd, new connection on new_fd struct sockaddr_in server_addr; // server address information struct sockaddr_in client_addr; // connector's address information socklen_t sin_size; int yes = 1; char buf[BUF_SIZE]; int ret; int i;
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); }
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt"); exit(1); }
server_addr.sin_family = AF_INET; // host byte order server_addr.sin_port = htons(MYPORT); // short, network byte order server_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));
if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind"); exit(1); }
if (listen(sock_fd, BACKLOG) == -1) { perror("listen"); exit(1); }
printf("listen port %d\n", MYPORT);
fd_set fdsr; int maxsock; struct timeval tv;
conn_amount = 0; sin_size = sizeof(client_addr); maxsock = sock_fd; while (1) { // initialize file descriptor set FD_ZERO(&fdsr); FD_SET(sock_fd, &fdsr);
// timeout setting tv.tv_sec = 30; tv.tv_usec = 0;
// add active connection to fd set for (i = 0; i < BACKLOG; i++) { if (fd_A[i] != 0) { FD_SET(fd_A[i], &fdsr); } }
ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv); if (ret < 0) { perror("select"); break; } else if (ret == 0) { printf("timeout\n"); continue; }
// check every fd in the set for (i = 0; i < conn_amount; i++) { if (FD_ISSET(fd_A[i], &fdsr)) { ret = recv(fd_A[i], buf, sizeof(buf), 0); if (ret <= 0) { // client close printf("client[%d] close\n", i); close(fd_A[i]); FD_CLR(fd_A[i], &fdsr); fd_A[i] = 0; } else { // receive data if (ret < BUF_SIZE) memset(&buf[ret], '\0', 1); printf("client[%d] send:%s\n", i, buf); } } }
// check whether a new connection comes if (FD_ISSET(sock_fd, &fdsr)) { new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size); if (new_fd <= 0) { perror("accept"); continue; }
// add to fd queue if (conn_amount < BACKLOG) { fd_A[conn_amount++] = new_fd; printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); if (new_fd > maxsock) maxsock = new_fd; } else { printf("max connections arrive, exit\n"); send(new_fd, "bye", 4, 0); close(new_fd); break; } } showclient(); }
// close other connections for (i = 0; i < BACKLOG; i++) { if (fd_A[i] != 0) { close(fd_A[i]); } }
exit(0); } |