一、C語言實現一個簡易的client/server聊天工具
在ubuntu平台上,采用c語言實現一個簡易的client/server聊天工具,思路是:
服務器端:首先創建一個服務器進程,該進程監聽客戶端的連接,如果收到並建立連接后創建一個線程服務該客戶端。該線程負責消息的轉發(這里為了方便直接對消息進行廣播)。
客戶端:客戶端進程首先創建一個線程用於消息接收處理,然后為用戶提供信息輸入的交互界面。
主要調用棧:
int socket( int domain, int type, int protocol)
- 功能:創建一個新的套接字,返回套接字描述符
- 參數說明:
- domain:域類型,指明使用的協議棧,如TCP/IP使用的是 PF_INET
- type: 指明需要的服務類型, 如
- SOCK_DGRAM: 數據報服務,UDP協議
- SOCK_STREAM: 流服務,TCP協議
- protocol:一般都取0
- 舉例:s=socket(PF_INET,SOCK_STREAM,0)
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)
- 功能: 同遠程服務器建立主動連接,成功時返回0,若連接失敗返回-1。
- 參數說明:
- Sockfd:套接字描述符,指明創建連接的套接字
- Server_addr:指明遠程端點:IP地址和端口號
- sockaddr_len :地址長度
int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
- 功能:為套接字指明一個本地端點地址TCP/IP協議使用sockaddr_in結構,包含IP地址和端口號,服務器使用它來指明熟知的端口號,然后等待連接
- 參數說明:
- Sockfd:套接字描述符,指明創建連接的套接字
- my_addr:本地地址,IP地址和端口號
- addrlen :地址長度
- 舉例:bind(sockfd, (struct sockaddr *)&address, sizeof(address));
int listen(int sockfd,int input_queue_size)
- 功能:
- 面向連接的服務器使用它將一個套接字置為被動模式,並准備接收傳入連接。用於服務器,指明某個套接字連接是被動的
- 參數說明:
- Sockfd:套接字描述符,指明創建連接的套接字
- input_queue_size:該套接字使用的隊列長度,指定在請求隊列中允許的最大請求數
- 舉例:listen(sockfd,20)
int accept(int sockfd, void *addr, int *addrlen);
- 功能:獲取傳入連接請求,返回新的連接的套接字描述符。為每個新的連接請求創建了一個新的套接字,服務器只對新的連接使用該套接字,原來的監聽套接字接受其他的連接請求。新的連接上傳輸數據使用新的套接字,使用完畢,服務器將關閉這個套接字。
- 參數說明:
- Sockfd:套接字描述符,指明正在監聽的套接字
- addr:提出連接請求的主機地址
- addrlen:地址長度
- 舉例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, &addrlen);
int send(int sockfd, const void * data, int data_len, unsigned int flags)
- 功能:在TCP連接上發送數據,返回成功傳送數據的長度,出錯時返回-1。send會將外發數據復制到OS內核中
- 參數說明:
- sockfd:套接字描述符
- data:指向要發送數據的指針
- data_len:數據長度
- flags:一直為0
- 舉例(p50):send(s,req,strlen(req),0);
int recv(int sockfd, void *buf, int buf_len,unsigned int flags);
- 功能:從TCP接收數據,返回實際接收的數據長度,出錯時返回-1。服務器使用其接收客戶請求,客戶使用它接受服務器的應答。如果沒有數據,將阻塞,如果收到的數據大於緩存的大小,多余的數據將丟棄。
- 參數說明:
- Sockfd:套接字描述符
- Buf:指向內存塊的指針
- Buf_len:內存塊大小,以字節為單位
- flags:一般為0
- 舉例:recv(sockfd,buf,8192,0)
close(int sockfd);
- 功能:撤銷套接字。如果只有一個進程使用,立即終止連接並撤銷該套接字,如果多個進程共享該套接字,將引用數減一,如果引用數降到零,則撤銷它。
- 參數說明:
- Sockfd:套接字描述符
- 舉例:close(socket_descriptor)
二、服務器端源代碼:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <string.h> 4 #include <sys/types.h> 5 #include <sys/socket.h> 6 #include <netinet/in.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 10 int client_sockfd[100] = {0};//客戶端套接字 11 int count = 0; 12 13 void* clientThreadLoop(void* local) 14 { 15 char buf[BUFSIZ]; //數據傳送的緩沖區 16 17 //printf("線程啟動\n"); 18 int socket_id = *(int *)local; 19 while(1) 20 { 21 memset(buf,0x00,sizeof(buf)); 22 int len=recv(socket_id,buf,BUFSIZ,0); 23 /*接收客戶端的數據並將其發送給客戶端--recv返回接收到的字節數,send返回發送的字節數*/ 24 if(len > 0) 25 { 26 buf[len]='\0'; 27 printf("服務器收到消息:%s\n",buf); 28 for(int i=0;i<count;i++) 29 { 30 send(client_sockfd[i],buf,len,0); 31 } 32 } 33 } 34 } 35 36 int main(int argc, char *argv[]) 37 { 38 int server_sockfd;//服務器端套接字 39 int len; 40 struct sockaddr_in my_addr; //服務器網絡地址結構體 41 struct sockaddr_in remote_addr = {0}; //客戶端網絡地址結構體 42 int sin_size; 43 memset(&my_addr,0,sizeof(my_addr)); //數據初始化--清零 44 my_addr.sin_family=AF_INET; //設置為IP通信 45 my_addr.sin_addr.s_addr=INADDR_ANY;//服務器IP地址--允許連接到所有本地地址上 46 my_addr.sin_port=htons(8000); //服務器端口號 47 48 /*創建服務器端套接字--IPv4協議,面向連接通信,TCP協議*/ 49 if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0) 50 { 51 perror("socket"); 52 return 1; 53 } 54 55 /*將套接字綁定到服務器的網絡地址上*/ 56 if (bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0) 57 { 58 perror("bind"); 59 return 1; 60 } 61 62 /*監聽連接請求--監聽隊列長度為5*/ 63 listen(server_sockfd,5); 64 65 sin_size=sizeof(struct sockaddr_in); 66 while(1) 67 { 68 /*等待客戶端連接請求到達*/ 69 if((client_sockfd[count]=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0) 70 { 71 perror("accept"); 72 return 1; 73 } 74 pthread_t tid; 75 printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr)); 76 len=send(client_sockfd[count],"Welcome to my server\n",21,0);//發送歡迎信息 77 pthread_create(&tid,NULL,clientThreadLoop,&client_sockfd[count]); 78 count++; 79 } 80 81 for(int j=0;j<count;j++) 82 close(client_sockfd[j]); 83 close(server_sockfd); 84 return 0; 85 }
調用accept()后進程阻塞等待,當連接成功后返回的套接字存入client_sockfd[count]數組,通過調用pthread_create()創建線程。
三、客戶端源代碼:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <string.h> 4 #include <sys/types.h> 5 #include <sys/socket.h> 6 #include <netinet/in.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 10 void* recThreadLoop(void* id) 11 { 12 char buffer[BUFSIZ]; 13 int cur_id = *(int *)id; 14 while(1) 15 { 16 memset(buffer,0x00,sizeof(buffer)); 17 int len=recv(cur_id,buffer,BUFSIZ,0); 18 buffer[len]='\0'; 19 printf("received:%s\n",buffer); 20 } 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 pthread_t tid; 26 int client_sockfd; 27 int len; 28 struct sockaddr_in remote_addr; //服務器端網絡地址結構體 29 char buf[BUFSIZ]; //數據傳送的緩沖區 30 memset(&remote_addr,0,sizeof(remote_addr)); //數據初始化--清零 31 remote_addr.sin_family=AF_INET; //設置為IP通信 32 remote_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服務器IP地址 33 remote_addr.sin_port=htons(8000); //服務器端口號 34 35 /*創建客戶端套接字--IPv4協議,面向連接通信,TCP協議*/ 36 if((client_sockfd=socket(PF_INET,SOCK_STREAM,0))<0) 37 { 38 perror("socket"); 39 return 1; 40 } 41 42 /*將套接字綁定到服務器的網絡地址上*/ 43 if(connect(client_sockfd,(struct sockaddr *)&remote_addr,sizeof(struct sockaddr))<0) 44 { 45 perror("connect"); 46 return 1; 47 } 48 printf("connected to server\n"); 49 len=recv(client_sockfd,buf,BUFSIZ,0);//接收服務器端信息 50 buf[len]='\0'; 51 printf("%s",buf); //打印服務器端信息 52 53 pthread_create(&tid,NULL,recThreadLoop,&client_sockfd); 54 55 /*循環的發送接收信息並打印接收信息--recv返回接收到的字節數,send返回發送的字節數*/ 56 while(1) 57 { 58 printf("Enter string to send:\n"); 59 scanf("%s",buf); 60 if(!strcmp(buf,"quit")) 61 break; 62 len=send(client_sockfd,buf,strlen(buf),0); 63 } 64 close(client_sockfd);//關閉套接字 65 return 0; 66 }
四、瀏覽器打開一個URL網址進行的步驟如下:
在DNS解析階段,需要用到如下調用棧:
- gethostname 獲得主機名
- getpeername 獲得與套接口相連的遠程協議地址
- getsockname 獲得套接口本地協議地址
- gethostbyname 根據主機名取得主機信息
- gethostbyaddr 根據主機地址取得主機信息
- getprotobyname 根據協議名取得主機協議信息
- getprotobynumber 根據協議號取得主機協議信息
- getservbyname 根據服務名取得相關服務信息
- getservbyport 根據端口號取得相關服務信息
- getsockopt/setsockopt 獲取/設置一個套接口選項
- ioctlsocket 設置套接口的工作方式
獲取到域名對應的IP之后便可以開始向服務器請求連接。
五、Socket系統調用
Socket系統調用的流程:
(1)系統調用 –> (2)查找socket –> (3)執行socket的對應操作函數 –> (4)執行傳輸層協議的對應操作函數;
內核源碼:
1 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 2 { 3 int retval; 4 struct socket *sock; 5 int flags; 6 7 ... 8 retval = sock_create(family, type, protocol, &sock); 9 if (retval < 0) 10 goto out; 11 12 retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); 13 if (retval < 0) 14 goto out_release; 15 16 out: 17 /* It may be already another descriptor 8) Not kernel problem. */ 18 return retval; 19 20 out_release: 21 sock_release(sock); 22 return retval; 23 }
可以看到socket函數主要由sock_create和sock_map_fd這兩個函數完成。