操作系统实验报告9:进程间通信—管道和 socket 通信
操作系統實驗報告9
實驗內容
- 實驗內容:進程間通信—管道和 socket 通信。
- 編譯運行課件 Lecture11 例程代碼:
- alg.11-3-socket-input-2.c
- alg.11-4-socket-connector-BBS-2.c
- alg.11-5-socket-server-BBS-3.c
- 編譯運行課件 Lecture11 例程代碼:
實驗環境
- 架構:Intel x86_64 (虛擬機)
- 操作系統:Ubuntu 20.04
- 匯編器:gas (GNU Assembler) in AT&T mode
- 編譯器:gcc
技術日志
實驗內容:進程間通信—管道和 socket 通信
- 驗證實驗Lecture11 例程代碼
分析:
實驗內容原理:
進程間通信—管道和socket通信常用的函數一般在頭文件#include<sys/socket.h>中里,其中有:
int socket(int domain, int type, int protocol)
socket()函數的作用是創建一個socket的描述符
其中參數domain是協議族,常用的協議有AF_INET(IPV4協議)、AD_INET6(IPV6協議)、AF_UNIX(單一Unix系統中進程間通信)、AF_LOCAL、AF_ROUTE等
type是套接口類型,常用值有SOCK_STREAM(流套接字,對應TCP協議)、SOCK_DGRAM(數據報套接字,對應UDP協議)、SOCK_RAW(原始套接字,提供原始網絡協議存取)
protocol是協議類型,一般取為0,會自動選擇type類型對應的協議,常用值有IPPROTO_TCP(對應TCP)、IPPROTO_UDP(對應UDP)、IPPROTO_ICMP(對應ICMP)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
bind()函數的作用是將一個地址族中的特定地址綁定給一套接字
其中參數sockfd代表要綁定的套接字的描述符,是之前通過socket()函數創建的;
addr表示服務器的通信地址,代表指向綁定給sockfd的協議地址的結構體;
addrlen是參數addr的長度,因為addr可以接受多種類型的結構體,所以需要addrlen額外指定結構體長度
int getsockname(int sockfd, struct sockaddr *restrict_addr, socklen_t *restrict_addrlen)
getsockname()函數的作用獲取一個套接字的名字
其中參數sockfd代表需要獲取名稱的套接字的標識符
restrict_addr是存放所獲取的套接字名稱的結構體
restrict_addrlen是參數addr的長度,因為addr可以接受多種類型的結構體,所以需要addrlen額外指定結構體長度
int listen(int sockfd, int backlog)
在使用socket()、bind()函數建立套接字并綁定后,套接字默認是主動類型,listen()函數的作用是監聽這個套接字,并把這個套接字變成被動類型,等待來自客戶端的連接請求
其中參數sockfd代表監聽的套接字的描述符
backlog表示相應的套接字可以排隊的最大連接個數
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen)
connect()函數的作用是客戶端可以通過調用這個函數和服務器發出連接請求建立連接
其中參數sockfd代表客戶端的socket的描述符
serv_addr表示服務器的socket地址
addrlen是套接字地址的長度
int accept(int sockfd, struct sockaddr restrict *addr, socklen_t restrict *addrlen)
accept()函數的作用是服務器通過listen()函數監聽到客戶端通過connect()函數發出的連接請求后,可以建立連接
其中參數sockfd代表服務器的套接字的描述符,也叫監聽套接字
addr表示客戶端的通信地址,存放在一個結構體里
addrlen是參數addr的長度
ssize_t send(int sockfd, const void *buf, socklen_t len, int flags)
send()函數的作用是向一個處在連接狀態的套接字發送數據
其中參數sockfd代表要發送給的套接字的描述符
buf表示存放要發送數據的一個緩沖區
len表示緩沖區中要發送的數據的字節數
flags是調用執行方式,一般是0,常用值有MSG_CONFIRM(用來告訴鏈路層),MSG_DONTWAIT(啟用非阻塞操作),MSG_DONTROUTE(只發送到直接連接的主機上)等。
ssize_t recv(int sockfd, void *buf, socklen_t len, int flags)
recv()函數的作用是令服務端或客戶端可以從另一端接收數據
其中參數sockfd代表接收端的套接字的描述符
buf表示存放接收來的數據的一個緩沖區
len表示緩沖區中數據的字節數
flags表示調用執行方式,一般置0
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
setsockopt()函數的作用是設置一個套接口的選項值
其中參數sockfd代表要設置的socket的描述符
level代表選項值定義的層次,值包括SOL_SOCKET和IPPROTO_TCP
optname要設置的套接口的選項
optval是指向存放設置的選項值的緩沖區,是一個指針
optlen是之前的參數optval的長度
下面兩個函數依賴的頭文件為#include <sys/types.h>和#include <ifaddrs.h>
int getifaddrs(struct ifaddrs **ifap)
getifaddrs()函數的作用是獲取本地網絡接口信息,儲存在一個鏈表中,鏈表的頭結點的地址保存在參數*ifap中
void freeifaddrs(struct ifaddrs *ifap)
freeifaddrs()函數的作用是將之前使用getifaddrs()函數動態分配內存獲得的參數ifap釋放掉
網絡編程的基本流程為:
這次的BBS程序中,服務端部分:
在alg.11-5-socket-server-BBS-3.c程序中,首先使用一個socket()函數創建了一個使用IPV4和TCP協議的套接字server_fd,設置服務端的IP地址和端口號后,使用bind()函數將服務器的地址綁定給套接字,接著使用listen()函數監聽是否有來自客戶端的連接請求,有請求后,使用accept()函數接收客戶端的連接請求并建立連接,建立連接后,使用recv_send_data()函數與客戶端使用命名管道進行發送和接收信息,最后使用close()函數關閉管道。
客戶端部分:
在alg.11-4-socket-connector-BBS-2.c程序中,也是首先使用一個socket()函數創建了一個使用IPV4和TCP協議的套接字connect_fd,設置這個客戶端的IP地址和端口號后,使用connect()函數向服務端發出連接請求并成功連接之后,創建一個子進程,實現發送消息和接收消息的進程同步進行,同步收發數據,當在alg.11-3-socket-input-2.c程序運行后,在其終端輸入信息,信息會通過管道傳輸到connector中,當輸入的數據是"#0"或者接收到的數據是"Console: #0"時,退出收發消息的循環,使用close()函數關閉套接字,并退出客戶端的進程。
程序總體結構:
首先運行alg.11-5-socket-server-BBS-3.c程序設置好端口號后建立一個server,server和自己設計的結構控制臺Console使用一個命名管道進行通信,管道文件名為test,控制臺Console用來輸出改名等等信息。
然后建立三個客戶端,分別運行alg.11-4-socket-connector-BBS-2.c程序和alg.11-3-socket-input-2.c程序三次,一個connector和一個input對應,并使用命名管道進行connector和input通信,三個客戶端的命名管道文件分別為test1、test2、test3,當有消息從input輸入后,會通過命名管道傳輸到對應的connector中,這三個客戶端的connector與服務端server建立連接后進行收發數據,server通過recv_send_data()函數使用匿名管道傳送消息,使用的是fd_msg,在pipe_data()函數處理完管道信息后,把結果又通過匿名管道fn[sn]使用recv_send_data()函數傳遞到第sn個客戶端中,實現聊天通信。
執行程序命令:
gcc -o server.o alg.11-5-socket-server-BBS-3.c gcc -o connector.o alg.11-4-socket-connector-BBS-2.c gcc -o input.o alg.11-3-socket-input-2.c./server.o test./connector.o test1 ./input.o test1./connector.o test2 ./input.o test2./connector.o test3 ./input.o test3執行截圖:
首先在一個終端輸入編譯命令獲得server、connector、input的.o文件,輸入./server.o test,彈出input server port number:的提示后輸入9000,使BBS的服務端運行起來,可以看到,輸出顯示Bind success! Listening ...,服務端正在等待客戶端發送消息
然后在另外兩個終端分別輸入./connector.o test1和./input.o test1,分別作為同一個客戶端的connector和input,在一個終端輸入./connector.o test1,提示Input server's hostname/ipv4:時,輸入服務端顯示的ipv4地址,這里是192.168.244.128,然后彈出提示Input server's port number:,輸入之前在服務端確定的端口號9000,客戶端就與服務端實現連接,這個connector對應的input就可以輸入消息了。
在第一個connector對應的input中,首先輸入#1xiaoming,可以看到,服務端中顯示第一個input對應的nickname由匿名Anonymous改為了xiaoming,同時向第一個input對應的connector發送nickname已經被修改的信息,并在前面加上Console:,提示是控制臺信息,不是用戶發送的聊天信息
在第二個和第三個connector中,按照之前開啟第一個客戶端的步驟開啟第二個和第三個客戶端,并把第二個客戶端的nickname命名為xiaohong,第三個客戶端的nickname命名為xiaogang。
在第一個input中,輸入聊天信息Hello World!,可以看到,服務端中顯示SN-1: Hello World!,表示用戶1xiaoming發送的信息Hello World!到達了服務端,然后服務端把信息儲存在緩沖區send_buf中,接著以廣播形式向所有的客戶端轉發,可以看到,在所有客戶端的connector中,都顯示了信息xiaoming: Hello World!
測試BBS系統的私聊功能,即輸入@+客戶的nickname+消息,可以單獨指定這個客戶發送消息,其它用戶不會收到消息,在第三個input中,輸入@xiaoming sayhello!,表示第三個客戶xiaogang向客戶nickname為xiaoming的用戶發送私聊消息sayhello!,可以看到,只有客戶1xiaoming收到了客戶3xiaogang發來的sayhello!的消息。
測試BBS的修改昵稱功能,在第三個input中,輸入#1xiaowei,表示第三個客戶xiaogang的nickname改為xiaowei,可以看到,服務端在收到改名的請求后,完成改名后,發送消息告訴第三個客戶nickname已經改為了xiaowei。
測試BBS的客戶端退出功能,在第一個input中,輸入#0,表示第一個客戶端退出聊天程序,可以看到,第一個connector退出進程,說明斷開連接,即使這時再在第一個input中輸入信息,服務端不會有任何顯示,其它客戶端也不會收到信息。
實現細節解釋:
// 文件alg.11-3-socket-input-2.c,充當輸入信息并通過管道將信息傳輸給客戶端connector的input角色 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h>#define BUFFER_SIZE 1024/* input terminal, data packed without '\n' */ /* write to pipe_data() through a named pipe */ int main(int argc, char *argv[]) {char fifoname[80], write_buf[BUFFER_SIZE];int fdw, flags, ret, i;if(argc < 2) {printf("Usage: ./a.out pathname\n");return EXIT_FAILURE;}strcpy(fifoname, argv[1]);// access()函數用來判斷是否有讀取文件的權限,其中參數fifoname代表FIFO文件路徑名,F_OK用來判斷FIFO文件是否存在,如果返回值為-1管道文件不存在,那么創建文件if(access(fifoname, F_OK) == -1) {// mkfifo()函數用來創建一個命名管道,權限為可讀可寫,如果返回值不為0,說明創建失敗,進行異常處理if(mkfifo(fifoname, 0666) != 0) {perror("mkfifo()");exit(EXIT_FAILURE);}elseprintf("new fifo %s created ...\n", fifoname);}// 用open()函數打開一個FIFO文件,其中fifoname是FIFO文件路徑名,O_RDWR代表權限可讀可寫,默認情況下以阻塞模式進行讀寫,當管道滿時阻塞,返回一個FIFO的文件描述符給fdwfdw = open(fifoname, O_RDWR);// 如果fdw小于0,說明打開管道失敗,進行錯誤處理if(fdw < 0) { perror("pipe open()");exit(EXIT_FAILURE);}// 否則打開管道成功else {// 使用fcntl()函數設置已打開的FIFO文件的文件性質,其中fdw是文件描述符,F_GETFL表示取得fdw對應的文件的打開方式狀態標志,最后一個參數可以默認為0flags = fcntl(fdw, F_GETFL, 0);// 使用fcntl()函數把文件標識符為fdw的FIFO文件的寫設置為非阻塞模式,其中F_SETFL表示設置文件打開方式為flags | O_NONBLOCK方式,即之前方式的基礎上加上非阻塞方式,當管道滿時直接返回-1fcntl(fdw, F_SETFL, flags | O_NONBLOCK);// 進入循環用于輸入信息while (1) {printf("Enter some text (#0-quit | #1-nickname): \n");// 先把用來存放寫入信息的緩沖區write_buf清空memset(write_buf, 0, BUFFER_SIZE);// 從標準輸入流即鍵盤向緩沖區write_buf寫入信息fgets(write_buf, BUFFER_SIZE, stdin);// 在緩沖區的末尾設置0,截斷可能溢出的信息write_buf[BUFFER_SIZE-1] = 0;// 把緩沖區中出現換行符的位置設置為0即結尾,過濾掉換行符for (i = 0; i < BUFFER_SIZE; i++) {if(write_buf[i] == '\n') {write_buf[i] = 0;}}// 使用非阻塞方式將緩沖區write_buf中的信息寫入命名管道中,并將write()函數的返回值賦給retret = write(fdw, write_buf, BUFFER_SIZE);// 如果ret小于等于0,說明管道已滿,暫時阻塞寫入,進程睡眠1s后再嘗試把信息寫入管道if(ret <= 0) {perror("write()");printf("Pipe blocked, try again ...\n");sleep(1);}}}//使用close()函數關閉管道的文件標識符close(fdw);exit(EXIT_SUCCESS); } // 文件alg.11-4-socket-connector-BBS-2.c,充當客戶端connector角色,用于接收input輸入的信息,并與服務端進行收發信息的通信 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <sys/signal.h> #include <fcntl.h> #include <sys/stat.h>#define BUFFER_SIZE 1024 #define NICKNAME_L 11 #define MSG_SIZE BUFFER_SIZE+NICKNAME_L+4 #define ERR_EXIT(m) \do { \perror(m); \exit(EXIT_FAILURE); \} while(0)/* asynchronous send-receive version; separated input terminal*/int main(int argc, char *argv[]) {char fifoname[80], nickname[80];int fdr, connect_fd;char ip_name_str[INET_ADDRSTRLEN];uint16_t port_num;char stdin_buf[BUFFER_SIZE], msg_buf[MSG_SIZE];int sendbytes, recvbytes, ret;char clr;struct hostent *host;struct sockaddr_in server_addr, connect_addr;socklen_t addr_len;pid_t childpid;if(argc < 2) {printf("Usage: ./a.out pathname\n");return EXIT_FAILURE;}strcpy(fifoname, argv[1]);// access()函數用來判斷是否有讀取文件的權限,其中參數fifoname代表FIFO文件路徑名,F_OK用來判斷FIFO文件是否存在,如果返回值為-1管道文件不存在,那么創建文件if(access(fifoname, F_OK) == -1) {// mkfifo()函數用來創建一個命名管道,權限為可讀可寫,如果返回值不為0,說明創建失敗,進行異常處理if(mkfifo(fifoname, 0666) != 0) {perror("mkfifo()");exit(EXIT_FAILURE);}elseprintf("new fifo %s named pipe created\n", fifoname);}// 用open()函數打開一個FIFO文件,其中fifoname是FIFO文件路徑名,O_RDWR代表權限可讀可寫,默認情況下以阻塞模式進行讀寫,返回一個FIFO的文件描述符給fdrfdr = open(fifoname, O_RDWR);if(fdr < 0) {perror("pipe read open()");exit(EXIT_FAILURE);}// www.baidu.com or an ipv4 addressprintf("Input server's hostname/ipv4: ");// 輸入服務端的主機名或ipv4地址到緩沖區stdin_buf中scanf("%s", stdin_buf);// 清空輸入緩沖區while((clr = getchar()) != '\n' && clr != EOF);printf("Input server's port number: ");// 輸入端口號到port_numscanf("%hu", &port_num);while((clr = getchar()) != '\n' && clr != EOF);// 使用gethostbyname()函數通過域名或主機名獲取IP地址if((host = gethostbyname(stdin_buf)) == NULL) {printf("invalid name or ip-address\n");exit(EXIT_FAILURE);}// 打印規范名printf("server's official name = %s\n", host->h_name);char** ptr = host->h_addr_list;for(; *ptr != NULL; ptr++) {inet_ntop(host->h_addrtype, *ptr, ip_name_str, sizeof(ip_name_str));printf("\tserver address = %s\n", ip_name_str);}// 建立客戶端的套接字,使用IPV4和TCP協議if((connect_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {ERR_EXIT("socket()");}// 設置IP地址類型為IPV4server_addr.sin_family = AF_INET;// 設置端口號,htons()函數用于將端口號從主機字節順序變成網絡字節順序server_addr.sin_port = htons(port_num);// 設置IP地址,從host的成員變量h_addr中獲取server_addr.sin_addr = *((struct in_addr *)host->h_addr);// 將server_addr的成員變量sin_zero的前八個字節清空bzero(&(server_addr.sin_zero), 8);addr_len = sizeof(struct sockaddr);// 使用connect()函數使客戶端可以和服務端發出請求建立連接ret = connect(connect_fd, (struct sockaddr *)&server_addr, addr_len);if(ret == -1) {close(connect_fd);ERR_EXIT("connect()"); }// 客戶端和服務端建立連接之后,客戶端會被分配一個端口addr_len = sizeof(struct sockaddr);// 使用getsockname()函數獲取客戶端的套接字的名字ret = getsockname(connect_fd, (struct sockaddr *)&connect_addr, &addr_len);if(ret == -1) {close(connect_fd);ERR_EXIT("getsockname()");}// 獲取客戶端被分配的端口號,使用ntohs()函數將一個16位數由網絡字節順序轉換為主機字節順序port_num = ntohs(connect_addr.sin_port);// 獲取客戶端的IP地址,使用inet_ntoa()函數將網絡地址轉換為"."點隔的字符串格式strcpy(ip_name_str, inet_ntoa(connect_addr.sin_addr));printf("Local port: %hu, IP addr: %s\n", port_num, ip_name_str);// 獲取服務端的IP地址strcpy(ip_name_str, inet_ntoa(server_addr.sin_addr));// 創建子進程childpid = fork();if(childpid < 0)ERR_EXIT("fork()");// 在父進程中if(childpid > 0) {// 進入一個發數據的循環中while(1) { /* sending cycle */// 以阻塞方式讀從命名管道中讀取數據,當管道滿時阻塞,數據從終端中輸入ret = read(fdr, stdin_buf, BUFFER_SIZE);if(ret <= 0) {perror("read()"); break;} stdin_buf[BUFFER_SIZE-1] = 0;// 使用send()函數以阻塞方式向客戶端的套接字發送數據sendbytes = send(connect_fd, stdin_buf, BUFFER_SIZE, 0);if(sendbytes <= 0) {printf("sendbytes = %d. Connection terminated ...\n", sendbytes);break;}// 如果輸入的數據是"#0",那么清空輸入緩沖區,以阻塞方式向客戶端的套接字發送消息"I quit ... ",然后退出if(strncmp(stdin_buf, "#0", 2) == 0) {memset(stdin_buf, 0, BUFFER_SIZE);strcpy(stdin_buf, "I quit ... ");sendbytes = send(connect_fd, stdin_buf, BUFFER_SIZE, 0);break;} }// 關閉管道close(fdr);// 關閉套接字close(connect_fd);// 結束子進程kill(childpid, SIGKILL);}// 在子進程中else {// 進入一個接收數據的循環中while(1) {// 使用recv()函數接收套接字的數據并存放到msg_buf中recvbytes = recv(connect_fd, msg_buf, MSG_SIZE, 0);if(recvbytes <= 0) {printf("recvbytes = %d. Connection terminated ...\n", recvbytes);break;}msg_buf[MSG_SIZE-1] = 0;// 打印接收到的數據printf("%s\n", msg_buf); // 如果傳送來的消息是"Console: #0",那么退出循環ret = strncmp(msg_buf, "Console: #0", 11);if(ret == 0) {break;}}// 關閉套接字close(connect_fd);// 終止當前進程kill(getppid(), SIGKILL);}return EXIT_SUCCESS; } // 文件alg.11-5-socket-server-BBS-3.c,服務端server角色 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <ifaddrs.h> #include <sys/shm.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/wait.h> #include <ctype.h>#define BUFFER_SIZE 1024 /* each pipe has at least 64 blocks for this sze */ #define NICKNAME_L 11 /* 10 chars for nickname */ #define MSG_SIZE BUFFER_SIZE+NICKNAME_L+4 /* msg exchanged between pipe_data() and recv_send_data() */ #define MAX_QUE_CONN_NM 5 /* length of ESTABLISHED queue */ #define MAX_CONN_NUM 10 /* cumulative number of connecting processes */ #define STAT_EMPTY 0 #define STAT_NORMAL 1 #define STAT_ENDED -1#define ERR_EXIT(m) \do { \perror(m); \exit(EXIT_FAILURE); \} while(0)/* one server, m clients BBS, with private chatting */struct {int stat;char nickname[NICKNAME_L]; } sn_attri[MAX_CONN_NUM+1];int connect_sn, max_sn; /* from 1 to MAX_CONN_NUM */ int server_fd, connect_fd[MAX_CONN_NUM+1]; int fd[MAX_CONN_NUM+1][2];/* ordinary pipe: pipe_data() gets max_sn from main() by fd[0][0]recv_send_data(sn) get send_buf from pipe_data() by fd[sn][0], 0<sn<MAX_CONN_NUM+1 */ int fd_stat[2]; /* ordinary pipe: pipe_data() gets stat of connect_sn from main() */ int fd_msg[2]; /* ordinary pipe: pipe_data() gets message of connect_sn from recv_send_data() */ int fdr; /* named pipe: pipe_data() gets stdin_buf from input terminal */ struct sockaddr_in server_addr, connect_addr;// 獲取ipv4地址,如果是ipv6地址的話也可以獲取ipv6地址 int getipv4addr(char *ip_addr) {struct ifaddrs *ifaddrsptr = NULL;struct ifaddrs *ifa = NULL;void *tmpptr = NULL;int ret;ret = getifaddrs(&ifaddrsptr);if(ret == -1)ERR_EXIT("getifaddrs()");for(ifa = ifaddrsptr; ifa != NULL; ifa = ifa->ifa_next) {if(!ifa->ifa_addr) {continue;}if(ifa->ifa_addr->sa_family == AF_INET) { /* IP4 */tmpptr = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;char addr_buf[INET_ADDRSTRLEN];inet_ntop(AF_INET, tmpptr, addr_buf, INET_ADDRSTRLEN);printf("%s IPv4 address %s\n", ifa->ifa_name, addr_buf);if(strcmp(ifa->ifa_name, "lo") != 0)strcpy(ip_addr, addr_buf); /* return the ipv4 address */} else if(ifa->ifa_addr->sa_family == AF_INET6) { /* IP6 */tmpptr = &((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;char addr_buf[INET6_ADDRSTRLEN];inet_ntop(AF_INET6, tmpptr, addr_buf, INET6_ADDRSTRLEN);printf("%s IPv6 address %s\n", ifa->ifa_name, addr_buf);}}if(ifaddrsptr != NULL) {freeifaddrs(ifaddrsptr);}return EXIT_SUCCESS; }// 處理管道數據 void pipe_data(void) { /* get sidin_buf from input terminalupdate max_sn from main()update sn_stat from main() - STAT_EMPTY->STAT_NORMALupdate sn_stat from recv_send_data() STAT_NORMAL->STAT_ENDEDupdate sn_nickname from recv_send_data()select connect_sn by the descritor @**** in start of send_buf */char send_buf[BUFFER_SIZE], stat_buf[BUFFER_SIZE], stdin_buf[BUFFER_SIZE];char msg_buf[MSG_SIZE]; /* sn(4)nickname(10)recv_buff(BUFFER_SIZE) */int flags, sn, ret, i, new_stat;char nickname[NICKNAME_L];// 使用fcntl()函數獲得fd[0][0]的文件狀態描述符flags = fcntl(fd[0][0], F_GETFL, 0);// 設置fd[0][0]使用非阻塞狀態讀取管道,當管道滿時直接返回-1fcntl(fd[0][0], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking read ord-pipe */// 使用fcntl()函數獲得fd_stat[0]的文件狀態描述符flags = fcntl(fd_stat[0], F_GETFL, 0);// 設置fd_stat[0]使用非阻塞狀態讀取管道,當管道滿時直接返回-1fcntl(fd_stat[0], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking read ord-pipe */// 使用fcntl()函數獲得fd_msg[0]的文件狀態描述符flags = fcntl(fd_msg[0], F_GETFL, 0);// 設置fd_msg[0]使用非阻塞狀態讀取管道,當管道滿時直接返回-1fcntl(fd_msg[0], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking read ord-pipe */// 使用fcntl()函數獲得fdr的文件狀態描述符flags = fcntl(fdr, F_GETFL, 0);// 設置fdr使用非阻塞狀態讀取管道,當管道滿時直接返回-1fcntl(fdr, F_SETFL, flags | O_NONBLOCK); /* set to non-blocking read nam-pipe */while(1) { while (1) { /* get the last current max_sn from main() */ret = read(fd[0][0], &sn, sizeof(sn)); /* non-blocking read ord-pipe from main() */if(ret <= 0) { /* pipe empty */break;} max_sn = sn;printf("max_sn changed to: %d\n", max_sn);}while (1) { /* update sn_stat from main() */ret = read(fd_stat[0], stat_buf, BUFFER_SIZE); /* non-blocking read ord-pipe from main() */if(ret <= 0) { /* pipe empty */break;} sscanf(stat_buf, "%d,%d", &sn, &new_stat);printf("SN stat changed: sn = %d, stat: %d -> %d\n", sn, sn_attri[sn].stat, new_stat);sn_attri[sn].stat = new_stat;} while (1) { /* update sn_stat and nickname from recv_send_data(), or brocast msg to all sn */ret = read(fd_msg[0], msg_buf, MSG_SIZE); /* non-blocking read ord-pipe from recv_send_data() */if(ret <= 0) { /* pipe empty */break;}sscanf(msg_buf, "%4d%s", &sn, stat_buf);if(msg_buf[4] == '#') {if(msg_buf[5] == '0') { /* #0: terminating the connect_fd */new_stat = STAT_ENDED;printf("SN stat changed: sn = %d, stat: %d -> %d\n", sn, sn_attri[sn].stat, new_stat);sn_attri[sn].stat = new_stat;}if(msg_buf[5] == '1') { /* #1name: renaming the nickname */strncpy(nickname, &msg_buf[6], NICKNAME_L);for (i = 0; i < NICKNAME_L-1; i++) {if(nickname[i] == ' ') {nickname[i] = '_';}if(nickname[i] == '\n') {nickname[i] = 0;}}nickname[i] = 0;printf("SN stat changed: sn = %d, nickname: %s -> %s\n", sn, sn_attri[sn].nickname, nickname);for (i=0; i<=max_sn; i++) { /* sn_attri[0].nickname = "Console" */ret = strcmp(sn_attri[i].nickname, nickname);if(ret == 0) {memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "Console: this nickname occupied: %s", nickname);ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);} break;}}if(i > max_sn) {strncpy(sn_attri[sn].nickname, nickname, NICKNAME_L);memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "Console: your nickname changed to %s", sn_attri[sn].nickname);ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);}}} /* ignore the message from recv_send_data() otherwise */}// 私聊功能,輸入@+要私聊的用戶的nickname,再輸入信息即可else if(msg_buf[4] == '@') {for (i = 0; i < NICKNAME_L-1; i++) {nickname[i] = msg_buf[5+i];if(msg_buf[5+i] == 0 || msg_buf[5+i] == ' ') {break;}}nickname[i] = 0;if(msg_buf[5+i] == ' ') {i++;}strcpy(stdin_buf, &msg_buf[5+i]);memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%s@: %s", sn_attri[sn].nickname, stdin_buf);for (sn = 1; sn <= max_sn; sn++) { /* message sent to all sn's by ord-pipes fd[sn][1] */if(sn_attri[sn].stat == STAT_NORMAL && strcmp(sn_attri[sn].nickname, nickname) == 0) {flags = fcntl(fd[sn][1], F_GETFL, 0);fcntl(fd[sn][1], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking write ord-pipe */ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);}}}}else {strcpy(stdin_buf, &msg_buf[4]);memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%s: %s", sn_attri[sn].nickname, stdin_buf);for (sn = 1; sn <= max_sn; sn++) { /* message sent to all sn's by ord-pipes fd[sn][1] */if(sn_attri[sn].stat == STAT_NORMAL) {flags = fcntl(fd[sn][1], F_GETFL, 0);fcntl(fd[sn][1], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking write ord-pipe */ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);}}}}}while (1) { /* read from input terminal and brocast to all sn */ret = read(fdr, stdin_buf, BUFFER_SIZE); /* non-blocking read nam-pipe from input terminal */if(ret <= 0) {break;} if(stdin_buf[0] == '@') {sn = atoi(&stdin_buf[1]);if(sn > 0 && sn <= max_sn && sn_attri[sn].stat == STAT_NORMAL) {for (i = 1; isdigit(stdin_buf[i]); i++) ;if(stdin_buf[i] == '#' && stdin_buf[i+1] == '0') { /* #0: terminating the connect_fd */new_stat = STAT_ENDED;printf("SN stat changed: sn = %d, stat: %d -> %d\n", sn, sn_attri[sn].stat, new_stat);sn_attri[sn].stat = new_stat;memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%s: %s", sn_attri[0].nickname, "#0 your connection terminated!");ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);};}else {flags = fcntl(fd[sn][1], F_GETFL, 0);fcntl(fd[sn][1], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking write ord-pipe */memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%s: %s", sn_attri[0].nickname, &stdin_buf[i]);ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);}}}; /* invalid connect_sn ignored */} else {memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%s: %s", sn_attri[0].nickname, stdin_buf);for (sn = 1; sn <= max_sn; sn++) { /* message sent to all sn's by ord-pipes fd[sn][1] */if(sn_attri[sn].stat == STAT_NORMAL) {flags = fcntl(fd[sn][1], F_GETFL, 0);fcntl(fd[sn][1], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking write ord-pipe */ret = write(fd[sn][1], msg_buf, MSG_SIZE); /* non-blocking write ord-pipe */if(ret <= 0) {printf("sn = %d write error, message missed ...\n", sn);}}}}}} return; }void recv_send_data(int sn) {char recv_buf[BUFFER_SIZE], send_buf[BUFFER_SIZE];char msg_buf[MSG_SIZE]; /* sn(4)nickname(10)recv_buff(BUFFER_SIZE) */int recvbytes, sendbytes, ret, flags;int stat;flags = fcntl(connect_fd[sn], F_GETFL, 0);fcntl(connect_fd[sn], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking mode to socket recv */flags = fcntl(fd[sn][0], F_GETFL, 0);fcntl(fd[sn][0], F_SETFL, flags | O_NONBLOCK); /* set to non-blocking mode to ord-pipe read */while(1) { /* receiving and sending cycle */recvbytes = recv(connect_fd[sn], recv_buf, BUFFER_SIZE, MSG_DONTWAIT); /* non-blocking socket recv */if(recvbytes > 0) {printf("===>>> SN-%d: %s\n", sn, recv_buf);memset(msg_buf, 0, MSG_SIZE);sprintf(msg_buf, "%4d%s", sn, recv_buf);ret = write(fd_msg[1], msg_buf, MSG_SIZE); /* blocking write ord-pipe to pipe_data() */if(ret <= 0) {perror("fd_stat write() to pipe_data()");break;}}ret = read(fd[sn][0], msg_buf, MSG_SIZE); /* non-blocking read ord-pipe from pipe_data() */if(ret > 0) {printf("sn = %d send_buf ready: %s\n", sn, msg_buf);sendbytes = send(connect_fd[sn], msg_buf, MSG_SIZE, 0); /* blocking socket send */if(sendbytes <= 0) {break;}} sleep(1); /* heart beating */}return; }int main(int argc, char *argv[]) {socklen_t addr_len;pid_t pipe_pid, recv_pid, send_pid;char stdin_buf[BUFFER_SIZE], ip4_addr[INET_ADDRSTRLEN];uint16_t port_num;int ret;char fifoname[80], clr;int stat;if(argc < 2) {printf("Usage: ./a.out pathname\n");return EXIT_FAILURE;}strcpy(fifoname, argv[1]);// access()函數用來判斷是否有讀取文件的權限,其中參數fifoname代表FIFO文件路徑名,F_OK用來判斷FIFO文件是否存在,如果返回值為-1管道文件不存在,那么創建文件if(access(fifoname, F_OK) == -1) {// mkfifo()函數用來創建一個命名管道,權限為可讀可寫,如果返回值不為0,說明創建失敗,進行異常處理if(mkfifo(fifoname, 0666) != 0) {perror("mkfifo()");exit(EXIT_FAILURE);}else {printf("new fifo %s named pipe created\n", fifoname);}}// 用open()函數打開一個FIFO文件,其中fifoname是FIFO文件路徑名,O_RDWR代表權限可讀可寫,默認情況下以阻塞模式進行讀寫,返回一個FIFO的文件描述符給fdrfdr = open(fifoname, O_RDWR);// 如果fdw小于0,說明打開管道失敗,進行錯誤處理,否則打開管道成功if(fdr < 0) {perror("named pipe read open()");exit(EXIT_FAILURE);}// 為fd數組中每個元素即管道標識符建立一個管道,如果建立失敗,打印錯誤原因for (int i = 0; i <= MAX_CONN_NUM; i++) {ret = pipe(fd[i]);if(ret == -1) {perror("fd pipe()");}}// 為fd_stat建立一個管道ret = pipe(fd_stat);if(ret == -1) {perror("fd_stat pipe()");}// 為fd_msg建立一個管道ret = pipe(fd_msg);if(ret == -1) {perror("fd_msg pipe()");}// 所有sn_attri數組元素的狀態都被初始化為STAT_EMPTY空狀態,昵稱為"Anonymous"匿名for (int i = 0; i <= MAX_CONN_NUM; i++) {sn_attri[i].stat = STAT_EMPTY;strcpy(sn_attri[i].nickname, "Anonymous");}// 第一個sn_attri數組元素的昵稱為"Console"控制臺strcpy(sn_attri[0].nickname, "Console");// 使用socket()函數創建一個socket的描述符返回給server_fd,AF_INET代表使用IPV4協議,SOCK_STREAM對應TCP協議,0為自動選擇協議類型,這里對應TCPserver_fd = socket(AF_INET, SOCK_STREAM, 0);// 創建失敗,就退出程序if(server_fd == -1) {ERR_EXIT("socket()");}printf("server_fd = %d\n", server_fd);// 獲取IPV4地址到ip4_addr中getipv4addr(ip4_addr);// 輸入不超過5位數的服務器端口號到port_numprintf("input server port number: ");memset(stdin_buf, 0, BUFFER_SIZE);fgets(stdin_buf, 6, stdin);stdin_buf[5] = 0;port_num = atoi(stdin_buf);// 設置服務端的IP地址類型為IPV4server_addr.sin_family = AF_INET;// 設置服務端的端口號,htons()函數用于將端口號從主機字節順序變成網絡字節順序server_addr.sin_port = htons(port_num);// 設置服務端的IP地址,inet_addr()函數用于將一個點分十進制的IP轉換成一個長整型整數server_addr.sin_addr.s_addr = inet_addr(ip4_addr);// 將server_addr的成員變量sin_zero的前八個字節清空bzero(&(server_addr.sin_zero), 8);int opt_val = 1;// 使用setsockopt()函數設置服務端的套接字使得相同的地址和端口可以被重用setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));// 使用bind()函數將服務端的地址綁定給套接字addr_len = sizeof(struct sockaddr);ret = bind(server_fd, (struct sockaddr *)&server_addr, addr_len);// 綁定失敗,關閉服務端,退出程序if(ret == -1) {close(server_fd);ERR_EXIT("bind()"); }printf("Bind success!\n");// 監聽與服務器地址綁定的套接字,等待來自客戶端的連接請求,可以排隊的最大連接個數為MAX_QUE_CONN_NMret = listen(server_fd, MAX_QUE_CONN_NM);// 綁定失敗,關閉服務端,退出程序if(ret == -1) {close(server_fd);ERR_EXIT("listen()");}printf("Listening ...\n");// 創建管道子進程,實現進程同步pipe_pid = fork();if(pipe_pid < 0) {close(server_fd);ERR_EXIT("fork()");}// 在子進程中使用管道處理數據if(pipe_pid == 0) {pipe_data();exit(EXIT_SUCCESS); /* ignoring all the next statements */}max_sn = 0;connect_sn = 1;while (1) {// 當連接的客戶端進程數超過最大值時,循環結束,退出聊天程序if(connect_sn > MAX_CONN_NUM) {printf("connect_sn = %d out of range\n", connect_sn);break;}// 每次accept()函數被調用時都要分配一次addr_len的大小addr_len = sizeof(struct sockaddr);// 服務端通過accept()函數接收第connect_sn個客戶端的連接請求后建立連接connect_fd[connect_sn] = accept(server_fd, (struct sockaddr *)&connect_addr, &addr_len);if(connect_fd[connect_sn] == -1) {perror("accept()");continue;}// 獲取要連接的客戶端的端口號到port_num,ntoh()函數用于將一個16位數由網絡字節順序轉換為主機字節順序port_num = ntohs(connect_addr.sin_port);// 獲取要連接的客戶端的地址到ip4_addr,inet_ntoa()函數用于將網絡地址轉換為"."點隔的字符串格式strcpy(ip4_addr, inet_ntoa(connect_addr.sin_addr));printf("New connection sn = %d, fd = %d, IP_addr = %s, port = %hu\n", connect_sn, connect_fd[connect_sn], ip4_addr, port_num);// 將服務端的狀態設置為已經和有的客戶端建立連接且還可以繼續其它客戶端建立連接的狀態stat = STAT_NORMAL;sprintf(stdin_buf, "%d,%d", connect_sn, stat);// 用緩沖區stdin_buf中的內容設置客戶端的狀態ret = write(fd_stat[1], stdin_buf, sizeof(stdin_buf)); /* blocking write ordinary pipe to pipe_data() */if(ret <= 0) {perror("fd_stat write() from recv_send_data() to pipe_data()");}// 申請一個用來傳輸數據的子進程recv_pid = fork();if(recv_pid < 0) {perror("fork()");break;}if(recv_pid == 0) {// 子進程用來向客戶端接收和發送數據recv_send_data(connect_sn);exit(EXIT_SUCCESS);}ret = max_sn = connect_sn;// 以阻塞方式將數據寫入管道,當管道滿時阻塞write(fd[0][1], &max_sn, sizeof(max_sn));if(ret <= 0) {perror("fd_stat write() from recv_send_data() to pipe_data()");}// 連接的客戶端數加一connect_sn++;// 此時父進程繼續監聽是否有新的客戶端請求連接}wait(0);// 關閉所有客戶端用到的管道for (int sn = 1; sn <= max_sn; sn++) {close(connect_fd[sn]);}// 關閉服務端close(server_fd);exit(EXIT_SUCCESS); }改進代碼:
在實驗中,要用到語句bzero(&(server_addr.sin_zero), 8)來將server_addr的成員變量sin_zero的前8個字節變成0,但是bzero并不是一個ANSI C函數,而在POSIX.1-2001標準里面,bzero函數已經被標記為了遺留函數,所以不推薦使用,在某些平臺或環境下運行可能會出現問題,和memset函數比起來,它唯一的好處是只有兩個參數,便于記憶,但是在寫程序時,為了更好地遵從規范,最好還是用memset函數代替bzero函數,可以使用宏定義替換程序各處出現的bzero:
#define bzero(s, n) memset(s, 0, n)總結
以上是生活随笔為你收集整理的操作系统实验报告9:进程间通信—管道和 socket 通信的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统实验报告8:进程间通信—消息机制
- 下一篇: 操作系统实验报告10:线程1