目錄
- 一、實驗準備
- 二、面向連接配接的流式套接字 C/S 例子
-
- 1.client
- 2.server
- 3.示範結果
- 三、非阻塞的多人聊天服務端示例
-
- 1.server
- 2.client
- 3.示範結果
- 四、參考
一、實驗準備
實驗目的:
驗證下列兩個SOCKET應用執行個體
- 面向連接配接的流式套接字 C/S 例子
- 非阻塞的多人聊天服務端示例
實驗環境:
Ubuntu、樹莓派
實驗開始前,pc和樹莓派連接配接同一個手機熱點,ubuntu設定為橋接模式。
手機在終端模拟器中執行指令
可以檢視連接配接熱點的裝置ip位址。
ip neigh
二、面向連接配接的流式套接字 C/S 例子
樹莓派我使用putty進行遠端登入,并把 Ubuntu 當做 client ,把樹莓派當做 server。
1.client
在ubuntu中使用指令
nano client.c
建立.c檔案并寫入以下代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT "9090" //the port client will be connecting to
#define MAXDATASIZE 100 //max number of bytes we can get at once
//get sockaddr, IPv4 or IPv6
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct addrinfo hints, *servinfo, *p;
/*
typedef struct addrinfo {
int ai_flags; //AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST
int ai_family; //AF_INET,AF_INET6
int ai_socktype; //SOCK_STREAM,SOCK_DGRAM
int ai_protocol; //IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6 etc.
size_t ai_addrlen; //must be zero or a null pointer
char* ai_canonname; //must be zero or a null pointer
struct sockaddr* ai_addr; //must be zero or a null pointer
struct addrinfo* ai_next; //must be zero or a null pointer
}
其中ai_flags、ai_family、ai_socktype說明如下:
參數 取值 值 說明
ai_family AF_INET 2 IPv4
AF_INET6 23 IPv6
AF_UNSPEC 0 協定無關
ai_protocol IPPROTO_IP 0 IP協定
IPPROTO_IPV4 4 IPv4
IPPROTO_IPV6 41 IPv6
IPPROTO_UDP 17 UDP
IPPROTO_TCP 6 TCP
ai_socktype SOCK_STREAM 1 流
SOCK_DGRAM 2 資料報
ai_flags AI_PASSIVE 1 被動的,用于bind,通常用于server socket
AI_CANONNAME 2
AI_NUMERICHOST 4 位址為數字串
*/
int rv;
char s[INET6_ADDRSTRLEN];
//如果指令行參數不等于 2 ,則執行下面的語句
if(argc != 2)
{
fprintf(stderr, "usage:client hostname\n"); //列印錯誤消息
exit(1); //退出
}
//将hints記憶體的内容置 0
memset(&hints, 0, sizeof hints);
//設定協定無關
hints.ai_family = AF_UNSPEC;
//設定套接為流
hints.ai_socktype = SOCK_STREAM;
/*
int getaddrinfo( const char *hostname, const char *service,
const struct addrinfo *hints, struct addrinfo **result );
參數說明
hostname:一個主機名或者位址串(IPv4的點分十進制串或者IPv6的16進制串)
service:服務名可以是十進制的端口号,也可以是已定義的服務名稱,如ftp、http等
hints:可以是一個空指針,也可以是一個指向某個addrinfo結構體的指針,
調用者在這個結構中填入關于期望傳回的資訊類型的暗示。
舉例來說:指定的服務既可支援TCP也可支援UDP,
是以調用者可以把hints結構中的ai_socktype成員設定成SOCK_DGRAM,
使得傳回的僅僅是适用于資料報套接口的資訊。
result:本函數通過result指針參數傳回一個指向addrinfo結構體連結清單的指針。
傳回值:0——成功,非0——出錯
getaddrinfo 函數能夠處理名字到位址以及服務到端口這兩種轉換,
傳回的是一個sockaddr結構的連結清單而不是一個位址清單。
這些sockaddr結構随後可由套接口函數直接使用。
如此一來,getaddrinfo函數把協定相關性安全隐藏在這個庫函數内部。
應用程式隻要處理由getaddrinfo函數填寫的套接口位址結構。
*/
if((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n",gai_strerror(rv));
return 1;
}
//周遊所有傳回結果并連結到第一個成功連接配接的套接
for(p = servinfo; p != NULL; p = p->ai_next)
{
//建立一個套接字
if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("client:socket");
continue;
}
//連接配接狀态判斷
if(connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("client:connect");
continue;
}
//如果建立套接字成功且連接配接成功,則退出循環
break;
}
//如果套接口位址為空,則列印結果
if(p == NULL)
{
fprintf(stderr, "client:failed to connect\n");
return 2;
}
//inet_ntop 函數可以将 IP 位址在“點分十進制”和“整數”之間轉換
inet_ntop(p->ai_family, get_in_addr((struct sockaddr*)p->ai_addr), s, sizeof s);
printf("client:connecting to %s\n",s);
//freeaddrinfo 函數釋放 getaddriinfo 函數傳回的存儲空間
freeaddrinfo(servinfo);
//recv 函數用于判斷緩沖區資料傳輸的狀态,傳輸異常則列印消息比并退出
if((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1)
{
perror("recv");
exit(1);
}
//将字元數組的最後一位置 \0 ,用于後面一次性輸出
buf[numbytes] = '\0';
printf("client:received %s\n",buf);
close(sockfd);
return 0;
}
儲存後進行編譯
gcc client.c -o client
2.server
在ubuntu中使用指令
nano server.c
建立.c檔案并寫入以下代碼
#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 <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "9090" //the port users will be connecting to
#define BACKLOG 10 //how many pending connections queue will hold
void sigchld_handler(int s)
{
//Waitpid temporarily stops the execution of the current process until a signal arrives or the child process terminates.
while(waitpid(-1, NULL, WNOHANG) > 0);
}
//get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
int sockfd, new_fd; //listen on sock_fd,new connection on new_fd
struct addrinfo hints, *servinfo, *p;
struct sockaddr_storage their_addr; //connector's address information
socklen_t sin_size;
//The data type "socklen_t" and int should have the same length.
//Otherwise, you break the padding of the BSD socket layer.
struct sigaction sa;
//Sigaction is a function that can be used to query or set up signal processing
int yes = 1;
char s[INET6_ADDRSTRLEN];
int rv;
//Set the Hints memory to zero
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; //use my IP
if((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n", gai_strerror(rv));
return 1;
}
//loop through all the results and bind to the first we can
for(p = servinfo; p != NULL; p = p->ai_next)
{
if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("server:socket");
continue;
}
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
{
perror("setsockopt");
exit(1);
}
if(bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("server:bind");
continue;
}
break;
}
//If the pointer P is null, an error message is printed
if(p == NULL)
{
fprintf(stderr, "server:failed to bind\n");
return 2;
}
//all done with this structure
freeaddrinfo(servinfo);
/*
Leave a socket in the state of listening for incoming connection requests
If the listening fails, exit
*/
if(listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}
sa.sa_handler = sigchld_handler; //reap all dead processes
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if(sigaction(SIGCHLD, &sa, NULL) == -1)
{
perror("sigaction");
exit(1);
}
printf("server:waiting for connections...\n");
//main accept() loop
while(1)
{
sin_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr*)&their_addr, &sin_size);
if(new_fd == -1)
{
perror("accept");
continue;
}
inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr*)&their_addr), s, sizeof s);
printf("server:got connection from %s\n",s);
if(!fork()) //this is the child process
{
close(sockfd); //child doesn't need the listener
if(send(new_fd, "Hello,world!", 13, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); //parent doesn't need this
}
return 0;
}
儲存後進行編譯編譯
gcc server.c -o server
3.示範結果
先運作樹莓派上的伺服器端程式,再運作ubuntu上的用戶端程式
三、非阻塞的多人聊天服務端示例
至少準備三個(Ubuntu 或者 樹莓派)Linux 系統:一個充當伺服器端,另外兩個充當用戶端(必須連上同一個手機熱點),代碼在下:
非阻塞的多人聊天 server 端,配合改良版 client 使用。
1.server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT "9090" //port we're listening on
//get sockaddr,IPv4 or IPv6:
//sockaddr 結構體:存儲參與(IP)Windows/linux套接字通信的計算機上的一個internet協定(IP)位址
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET){
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
fd_set master; //主檔案描述符清單
fd_set read_fds; //select() 的臨時檔案描述符清單
/*
當調用 select() 時,由核心根據 IO 狀态修改 fd_set 的内容
由此來通知執行了 select() 的程序哪一 socket 或檔案發生了可讀或可寫事件
*/
int fdmax; //最大檔案描述符
int listener; //監聽套接字描述符
int newfd; //新接受的套接字描述符
//sockaddr_storage 結構體:存儲套接字位址資訊
struct sockaddr_storage remoteaddr; //用戶端位址
socklen_t addrlen;//socklen_t 和 int 相同長度的一種類型
char buf[256]; //用于用戶端資料的緩沖區
int nbytes;
int yes=1; //for setsockopt() SO_REUSEADDR,below
int i,j,rv;
char remoteIP[INET_ADDRSTRLEN];
struct addrinfo hints,*ai,*p; //位址資訊結構體
FD_ZERO(&master); //清除主檔案描述符清單
FD_ZERO(&read_fds); //清楚臨時檔案描述符清單
//給我們一個套接字并綁定它
memset(&hints, 0, sizeof hints); //位址資訊置零
hints.ai_family = AF_UNSPEC; //AF_UNSPEC(協定無關)
hints.ai_socktype = SOCK_STREAM; //SOCK_STREAM(流)
hints.ai_flags = AI_PASSIVE; //AI_PASSIVE(被動的,用于 bind)
if((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0)
{
fprintf(stderr, "selectserver:%s\n", gai_strerror(rv));
exit(1);
}
//周遊所有的結果
for(p = ai; p != NULL; p = p->ai_next)
{
//建立套接字并指派給 listener 套接字描述符
listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if(listener < 0)
{
continue;
}
//setsockope 函數用于任意類型、任意狀态套接字的設定選項
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
/*
bind 函數:用于未連接配接的資料報或流類套接口,把一本位址與一套接口捆綁
在 connect() 或 listen() 調用前使用
*/
if(bind(listener, p->ai_addr, p->ai_addrlen) < 0)
{
close(listener);
continue;
}
break;
}
if(p == NULL)
{
fprintf(stderr, "selectserver:failed to bind\n");
exit(2);
}
//到了這裡,意味着伺服器的位址和端口綁定完成,可以釋放 ai 的記憶體
freeaddrinfo(ai);
/*
listen 函數:建立一個套接口并監聽申請的連接配接.
參數一:用于辨別一個已捆綁未連接配接套接口的描述符
參數二:等待連接配接隊列的最大長度(這裡是 10)
*/
if(listen(listener, 10) == -1)
{
perror("listen");
exit(3);
}
//将偵聽器添加到主檔案
FD_SET(listener, &master);
//跟蹤最大的檔案描述符
fdmax = listener; //到目前為止,是這個
//主循環
for(;;)
{
read_fds = master; //将主檔案描述符表複制到臨時檔案描述符表
/*
select 函數:确定一個或多個套接口的狀态,如需要則等待。
原型:int select( int nfds, fd_set FAR* readfds, fd_set * writefds,
fd_set * exceptfds, const struct timeval * timeout);
nfds:是一個整數值,是指集合中所有檔案描述符的範圍,即所有檔案描述符的最大值加1,
不能錯!在Windows中這個參數的值無所謂,可以設定不正确。
readfds:(可選)指針,指向一組等待可讀性檢查的套接口。
writefds:(可選)指針,指向一組等待可寫性檢查的套接口。
exceptfds:(可選)指針,指向一組等待錯誤檢查的套接口。
timeout:select()最多等待時間,對阻塞操作則為 NULL。
*/
if(select(fdmax + 1, &read_fds, NULL, NULL, NULL) == -1)
{
perror("select");
exit(4);
}
//運作現有的連接配接,查找要讀取的資料
for(i = 0; i <= fdmax; i++)
{
/*
宏原型:int FD_ISSET(int fd,fd_set *fdset)
在調用 selelct() 函數後,用 FD_ISSET 來檢測 fd 在 fdset 集合中的狀态是否變化
傳回整形,當檢測到 fd 狀态發生變化時傳回真,否則傳回假
*/
if(FD_ISSET(i, &read_fds)) //得到了一個連接配接
{
if(i == listener)//如果新連接配接為最大檔案描述符
{
//處理新連接配接
addrlen = sizeof remoteaddr;
newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen);
if(newfd == -1)
{
perror("accept");
}
else
{
FD_SET(newfd, &master); //添加到主檔案描述符清單
if(newfd > fdmax) //記錄最大值
{
fdmax = newfd;
}
printf("selectserver:new connection from %s on socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET_ADDRSTRLEN), newfd);
}
}
else
{
//處理來自用戶端的資料
if((nbytes = recv(i, buf, sizeof buf, 0)) <= 0)
{
//出現錯誤或連接配接被用戶端關閉
if(nbytes == 0)
{
//連接配接關閉了
printf("selectserver:socket %d hung up\n", i);
}
else
{
perror("recv");
}
close(i); //關閉
FD_CLR(i, &master); //從主檔案描述符清單中删除
}
else
{
//我們從一個客戶那裡得到了一些資料
for(j =0; j <= fdmax; j++)
{
//發給大家!
if(FD_ISSET(j, &master))
{
//除了監聽器和我們自己
if(j != listener && j != i)
{
if(send(j, buf, nbytes, 0) == -1)
{
perror("send");
}
}
}
}
}
} //END handle from client
} //END got new incoming connection
} //END looping through file descriptors
} //END for(;;)--and you thought it would never end!
return 0;
}
2.client
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <syspes.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT "9090" //the port client will be connecting to
#define MAXDATASIZE 100 //max number of bytes we can get at once
int sockfd, numbytes;
char buf[MAXDATASIZE];
//get sockaddr, IPv4 or IPv6
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
void *recvMag()
{
while(1)
{
if((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1)
{
perror("recv");
exit(1);
}
if(numbytes == 1)
continue;
buf[numbytes] = '\0';
printf("\nreceived:%s\n",buf);
}
}
int main(int argc, char *argv[])
{
struct addrinfo hints, *servinfo, *p;
int rv;
char s[INET6_ADDRSTRLEN];
pthread_t t1;
char mag[MAXDATASIZE];
if(argc != 2)
{
fprintf(stderr, "usage:client hostname\n");
exit(1);
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n",gai_strerror(rv));
return 1;
}
for(p = servinfo; p != NULL; p = p->ai_next)
{
if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("client:socket");
continue;
}
if(connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("client:connect");
continue;
}
break;
}
if(p == NULL)
{
fprintf(stderr, "client:failed to connect\n");
return 2;
}
inet_ntop(p->ai_family, get_in_addr((struct sockaddr*)p->ai_addr), s, sizeof s);
printf("client:connecting to %s\n",s);
freeaddrinfo(servinfo);
int err = pthread_create(&t1, NULL, recvMag, NULL);
if(err != 0)
{
printf("receive failed");
exit(1);
}
while(1)
{
scanf("%s", mag);
if(send(sockfd, mag, sizeof mag, 0) == -1)
{
printf("send failed!\n");
}
}
//close(sockfd);
return 0;
}
3.示範結果
先編譯執行伺服器端代碼,再編譯執行用戶端的,當想退出程式的時候,按 Ctrl + C 即可退出。
伺服器端:
gcc chatserver.c -o chatserver
./chatserver
1、由于 Linux 沒有 pthread 庫,而用戶端運用到了多線程,使用了 pthread_create 函數,是以編譯的時候需要加上 pthread 庫。
2、執行檔案的時候,後面需要跟上伺服器的 IP 位址作為參數。
用戶端1:
gcc -pthread -o s s.c
./s xxx.xxx.xxx.xxx//伺服器ip
用戶端2:
gcc -pthread -o 1 1.c
./1 xxx.xxx.xxx.xxx//伺服器ip
四、參考
Ubuntu 與樹莓派之間的兩個 socket 應用執行個體:C/S 和多人聊天