/* common.h */ /*服務器端口信息*/ #define PORTLINK ".charport" /*緩存限制*/ #define MAXNAMELEN 256 #define MAXPKTLENE 2048 /*信息類型釘釘*/ #define LIST_GROUPS 0 #define JOIN_GROUP 1 #define LEAVE_GROUP 2 #define USER_TXET 3 #define JOIN_REJECTED 4 #define JOIN_ACCEPTED 5 /*數據包結構*/ typedef struct _packet { /* 數據包類型 數據報類型長度 數據報內容 */ char type; long lent; char *text; }Packet; extern int startserver(); extern int locateserver(); extern Packet *recvpkt(int sd); extern int sendpkt(int sd, char typ, long len, char *buf); extern void freepkt(Packet *msg);
/* ChatLinker.c */ #include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include<string.h> #include<strings.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<netdb.h> #include<time.h> #include<errno.h> #include"common.h" /* 爲服務器接收客戶端請求作準備, 正確返回 socket 文件描述符 錯誤返回 -1 */ int startserver() { int sd; /* socket 描述符 */ int myport; /* 服務器端口 */ const char *myname; /* 本地主機的全稱 */ char linktrgt[MAXNAMELEN]; char linkname[MAXNAMELEN]; /* 調用 socket 函數建立 TCP socket 描述符 */ sd = socket(PF_INET, SOCK_STREAM, 0); /* 調用bind函數將一個本地地址指派給 socket */ struct sockaddr_in server_address; server_address.sin_family = AF_INET; /* 通配地址 INADDR_ANY 表示IP地址爲 0.0.0.0, 內核在套接字被鏈接後選擇一個本地地址 htonl函數 用於將 INADDR_ANY 轉換爲網絡字節序 */ server_address.sin_addr.s_addr = htonl(INADDR_ANY); /* 指派爲通配端口 0,調用 bind 函數後內核將任意選擇一個臨時端口 */ server_address.sin_port = htons(0); bind(sd, (struct sockaddr *)&server_address, sizeof(server_address)); /* 調用listen 將服務器端 socket 描述符 sd 設置爲被動地監聽狀態 並設置接受隊列的長度爲20 */ listen(sd,20); /* 調用 getsockname、gethostname 和 gethostbyname 肯定本地主機名和服務器端口號 */ char hostname[MAXNAMELEN]; if (gethostname(hostname, sizeof(hostname)) != 0) { perror("gethostname"); } struct hostent *h; h = gethostbyname(hostname); int len = sizeof(struct sockaddr); getsockname(sd, (struct sockaddr *)&server_address, &len); myname = h->h_name; myport = ntohs(server_address.sin_port); /* 在家目錄下建立符號連接'.chatport'指向linktrgt */ sprintf(linktrgt, "%s:%d", myname, myport); /* 在頭文件 common.h 中:#define PORTLINK ".chatport" */ sprintf(linkname ,"%s/%s", getenv("HOME"), PORTLINK); if (symlink(linktrgt,linkname) != 0) { fprintf(stderr, "error : server already exists\n"); return -1; } /* 準備接受客戶端請求 */ printf("admin: started server on '%s' at '%d'\n", myname, myport); return sd; } /* 和服務器創建鏈接,正確返回 socket 描述符, 失敗返回 -1 */ int hooktoserver() { int sd; char linkname[MAXNAMELEN]; char linktrgt[MAXNAMELEN]; char *servhost; char *servport; int bytecnt; /* 獲取服務器地址 */ sprintf(linkname, "%s/%s", getenv("HOME"), PORTLINK); bytecnt = readlink(linkname, linktrgt, MAXNAMELEN); if (bytecnt == -1) { fprintf(stderr,"error : no active char server\n"); return -1; } linktrgt[bytecnt] = '\0'; /* 得到服務器 IP 地址和端口號 */ servport = index(linktrgt, ':'); *servport = '\0'; servport++; servhost = linktrgt; /* 得到服務器 IP 地址的 unsigned short 形式 */ unsigned short number = (unsigned short) strtoul(servport, NULL, 0); /* 調用函數 socket 建立 TCP 套接字 */ sd = socket(AF_INET, SOCK_STREAM, 0); /* 調用 gethostbyname() 和 connect()鏈接 'servhost' 的 'servport' 端口 */ struct hostent *hostinfo; struct sockaddr_in address; hostinfo = gethostbyname(servhost); address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list; address.sin_family = AF_INET; address.sin_port = htons(number); if (connect(sd, (struct sockaddr *) &address, sizeof(address)) < 0) { perror("connecting"); exit(1); } printf("admin : connected to server on '%s' at '%s'\n", servhost,servport); return sd; } /* 從內核讀取一個套接字的信息 */ int readn(int sd, char *buf, int n) { int toberead; char *ptr; toberead = n; ptr = buf; while (toberead > 0) { int byteread; byteread = read(sd, ptr, toberead); if (byteread <= 0) { if(-1 == byteread) { perror("read"); } return 0; } toberead -= byteread; ptr += byteread; } return 1; } /* 接收數據包 */ Packet *recvpkt(int sd) { Packet *pkt; pkt = (Packet *)calloc(1, sizeof(Packet)); if (!pkt) { fprintf(stderr, "error : unable to calloc \n"); return NULL; } /* 讀取消息類型 */ if (!readn(sd, (char *)&pkt->type, sizeof(pkt->type))) { free(pkt); return NULL; } /* 讀取消息長度 */ if (!readn(sd, (char *)&pkt->lent, sizeof(pkt->lent))) { free(pkt); return NULL; } pkt->lent = ntohl(pkt->lent); /* 讀取消息文本 */ if (pkt->lent > 0) { pkt->text = (char *)malloc(pkt->lent); if (!pkt) { fprintf(stderr,"error : unable to malloc\n"); return NULL; } if (!readn(sd, pkt->text, pkt->lent)) { freepkt(pkt); return NULL; } } return pkt; } /* 發送數據包 */ int sendpkt(int sd, char typ, long len, char *buf) { char tmp[8]; long siz; /* 把包的類型和長度寫入套接字 */ bcopy(&typ, tmp, sizeof(typ)); siz = htonl(len); bcopy((char *) &siz, tmp + sizeof(typ), sizeof(len)); write(sd, tmp, sizeof(typ)+sizeof(len)); /* 把消息文本寫入套接字 */ if (len > 0) { write(sd, buf, len); } return 1; } /* 釋放數據包占用的內存空間 */ void freepkt(Packet *pkt) { free(pkt->text); free(pkt); }
/* ChatServer.c */ #include<stdio.h> #include<signal.h> #include<stdlib.h> #include<fcntl.h> #include<string.h> #include<strings.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<netdb.h> #include<time.h> #include<errno.h> #include"common.h" /* 聊天室成員信息 */ typedef struct _member { char *name; /*成員姓名*/ int sock; /*成員socket描述符*/ int grid; /*成員所屬聊天室*/ struct _member *next; /*下一個成員*/ struct _member *prev; /*前一個成員*/ }Member; /* 聊天室信息 */ typedef struct _group { char *name; /* 聊天室名字 */ int capa; /* 聊天室最大容量(人數) */ int occu; /* 當前佔有率(人數) */ struct _member *mems; /* 記錄聊天室內全部成員信息的鏈表 */ }Group; /* 全部聊天室的信息表 */ Group *group; int ngroups; /* 經過聊天室名字找到聊天室 ID */ int findgroup(char *name) { int grid; for (grid = 0; grid < ngroups; grid++) { if (strcmp(group[grid].name, name) == 0) { return grid; } } return -1; } /* 經過室成員名字找到室成員的信息 */ Member *findmemberbyname(char *name) { int grid; for (grid = 0; grid < ngroups; grid++) { Member *memb; for (memb = group[grid].mems; memb; memb = memb->next) { if (0 == strcmp(memb->name, name)) { return memb; } } } return NULL; } /* 經過 socket 描述符找到室成員的信息 */ Member *findmemberbysock(int sock) { int grid; for (grid = 0 ; grid < ngroups; grid++) { Member *memb; for (memb = group[grid].mems; memb; memb = memb->next) { if (memb->sock == sock) { return memb; } } } return NULL; } /* 退出前的清理工做 */ void cleanup() { char linkname[MAXNAMELEN]; sprintf(linkname,"%s/%s",getenv("HOME"),PORTLINK); unlink(linkname); exit(0); } int initgroups(char *groupsfile) { FILE *fp; char name[MAXNAMELEN]; int capa; int grid; /* 打開存儲聊天室信息的配置文件 */ fp = fopen(groupsfile,"r"); if (!fp) { fprintf(stderr,"error : unable to open file '%s'\n",groupsfile); return 0; } /* 從配置文件中讀取聊天室的數量 */ fscanf(fp,"%d",&ngroups); group = (Group *)calloc(ngroups,sizeof(Group)); if (!group) { fprintf(stderr,"error : unable to calloc\n"); return 0; } /* 從配置文件讀取聊天室信息 */ for (grid = 0; grid < ngroups; grid++) { /* 讀取聊天室名和容量 */ if (fscanf(fp,"%s %d", name, &capa) != 2) { fprintf(stderr, "error : no info on froup %d\n",grid + 1); return 0; } /* 將信息存進 group 結構 */ group[grid].name = strdup(name); group[grid].capa = capa; group[grid].occu = 0; group[grid].mems = NULL; } return 1; } int main(int argc, char *argv[]) { int servsock; /* 聊天室服務器端監聽 socket 描述符 */ int maxsd; /* 鏈接的客戶端 socket 描述符的最大值 */ fd_set livesdset, tempset; /* 客戶端 sockets 描述符集 */ if (argc != 2) { fprintf(stderr,"usage : %s <group-file>\n",argv[0]); exit(1); } /* 調用 initgroups 函數,初始化聊天室信息 */ if (!initgroups(argv[1])) { exit(1); } /* 設置信號處理函數 */ signal(SIGTERM, cleanup); signal(SIGINT,cleanup); /* 準備接受請求 */ /* 定義在 "chatlinker.c" 文件中 主要完成建立服務器套接字,綁定端口號, 並設置把套接字爲監聽狀態 */ servsock = startserver(); if (-1 == servsock) { return -1; } maxsd = servsock; /* 初始化描述符集 */ FD_ZERO(&livesdset); FD_ZERO(&tempset); /* 打開服務器監聽套接字的套接字 描述符 servsock 對應的fd_set 比特位 */ FD_SET(servsock, &livesdset); while(1) { int sock; /* 特別注意 tempset 做爲 select 參數時是一個 "值-結果" 參數, select 函數返回時,tempset 中打開的比特位只是讀就緒的 socket 描述符,因此咱們每次循環都要將其更新爲咱們須要內核測試讀就緒條件 的 socket 描述符集合 livesdset */ tempset = livesdset; /* 調用 select 函數等待已鏈接套接字上的包和來自 新的套接字的連接請求 */ select(maxsd+1, &tempset, NULL, NULL, NULL); /* 循環查找來自客戶機的請求 */ for (sock = 3; sock <= maxsd; sock++) { /* 若是是服務器監聽 socket,則跳出接收數據包環節,執行接受鏈接 */ if (sock == servsock) { continue; } /* 有來自客戶 socket 的消息 */ if (FD_ISSET(sock, &tempset)) { Packet *pkt; pkt = recvpkt(sock); if (!pkt) { /* 客戶機斷開了鏈接 */ char *clientname; /* 使用 gethostbyaddr,getpeername 函數獲得 client 的主機名 */ socklen_t len; struct sockaddr_in addr; len = sizeof(addr); if (getpeername(sock, (struct sockaddr *)&addr, &len) == 0) { struct sockaddr_in *s = (struct sockaddr_in *)&addr; struct hostent *he; he = gethostbyaddr(&s->sin_addr, sizeof(struct in_addr), AF_INET); clientname = he->h_name; } else { printf("Cannot get peer name"); } printf("admin : disconnect from '%s' at '%d'\n", clientname, sock); leavegroup(sock); close(sock); FD_CLR(sock, &livesdset); } else { char *gname, *mname; /* 基於消息類型採起行動 */ switch (pkt->type) { case LIST_GROUPS: listgroups(sock); break; case JOIN_GROUP: gname = pkt->text; mname = gname + strlen(gname) + 1; joingroup(sock, gname, mname); break; case LEAVE_GROUP: leavegroup(sock); break; case USER_TXET: relaymsg(sock, pkt->text); break; } freepkt(pkt); } } } struct sockaddr_in remoteaddr; /* 客戶機地址結構 */ socklen_t addrlen; /* 有來自新的客戶機的鏈接請求請求 */ if (FD_ISSET(servsock, &tempset)) { /* 已鏈接的 socket 描述符 */ int csd; /* 接受一個新的鏈接請求 */ addrlen = sizeof(remoteaddr); csd = accept(servsock, (struct sockaddr *)&remoteaddr, &addrlen); if (-1 != csd) { char *clientname; struct hostent *h; /* 使用 gethostbyaddr 函數獲得 client 的主機名 */ h = gethostbyaddr((char *)&remoteaddr.sin_addr.s_addr, sizeof(struct in_addr), AF_INET); if (h != (struct hostent *)0) { clientname = h->h_name; } else { printf("gethostbyaddr failed\n"); } /* 顯示客戶機的主機名和對應的 socket 描述符 */ printf("admin : connect from '%s' at '%d'\n", clientname, csd); /* 將該鏈接的套接字描述符 csd 加入livesdset */ FD_SET(csd, &livesdset); /* 保持 maxsd 記錄的是最大的套接字描述符 */ if (csd > maxsd) { maxsd = csd; } } else { perror("accept"); exit(0); } } } } /* 把全部聊天室的信息發給客戶端 */ int listgroups(int sock) { int grid; char pktbufr[MAXPKTLENE]; char *bufrptr; long bufrlen; /* 每一塊信息在字符串中用 NULL 分割 */ bufrptr = pktbufr; for (grid = 0; grid < ngroups; grid++) { /* 獲取聊天室名字 */ sprintf(bufrptr,"%s", group[grid].name); bufrptr += strlen(bufrptr) + 1; /* 獲取聊天室容量 */ sprintf(bufrptr, "%d", group[grid].capa); bufrptr += strlen(bufrptr) + 1; /* 獲取聊天室佔有率 */ sprintf(bufrptr, "%d",group[grid].occu); bufrptr += strlen(bufrptr) + 1; } bufrlen = bufrptr - pktbufr; /* 發送消息給回覆客戶機的請求 */ sendpkt(sock, LIST_GROUPS, bufrlen, pktbufr); return 1; } /* 加入聊天室 */ int joingroup(int sock, char *gname, char *mname) { int grid; Member *memb; /* 根據聊天室名得到聊天室 ID */ grid = findgroup(gname); if (grid == -1) { char *errmsg = "no such group"; sendpkt(sock, JOIN_REJECTED, strlen(errmsg), errmsg); return 0; } /* 檢查是否聊天室成員名字已被佔用 */ memb = findmemberbyname(mname); /* 若是聊天室成員名已存在,則返回錯誤消息 */ if (memb) { char *errmsg = "member name already exists"; sendpkt(sock, JOIN_REJECTED, strlen(errmsg), errmsg); return 0; } /* 檢查聊天室是否已滿 */ if (group[grid].capa == group[grid].occu) { char *errmsg = "room is full"; sendpkt(sock, JOIN_REJECTED, strlen(errmsg), errmsg); return 0; } memb = (Member *)calloc(1, sizeof(Member)); if (!memb) { fprintf(stderr, "error : unable to calloc\n"); cleanup(); } memb->name = strdup(mname); memb->sock = sock; memb->grid = grid; memb->prev = NULL; memb->next = group[grid].mems; if (group[grid].mems) { group[grid].mems->prev = memb; } group[grid].mems = memb; printf("admin: '%s' join '%s'\n",mname, gname); /* 更新聊天室的在線人數 */ group[grid].occu++; sendpkt(sock, JOIN_ACCEPTED, 0, NULL); return 1; } /* 離開聊天室 */ int leavegroup(int sock) { Member *memb; /* 獲得聊天室成員信息 */ memb = findmemberbysock(sock); if (!memb) { return 0; } /* 從聊天室信息結構中刪除 memb 成員 */ if (memb->next) { memb->next->prev = memb->prev; /* 在聊天室成員鏈表的尾部 */ } if (group[memb->grid].mems == memb) { group[memb->grid].mems = memb->next; /* 在聊天室成員鏈表的頭部 */ } else { memb->prev->next = memb->next; /* 在聊天室成員鏈表的中部 */ } printf("admin: '%s' left '%s'\n", memb->name, group[memb->grid].name); /* 更新聊天室的佔有率 */ group[memb->grid].occu--; free(memb->name); free(memb); return 1; } /* 把成員的消息發送給其餘聊天室成員 */ int relaymsg(int sock, char *text) { Member *memb; Member *sender; char pktbufr[MAXPKTLENE]; char *bufrptr; long bufrlen; /* 根據 socket 描述符得到該聊天室成員的信息 */ sender = findmemberbysock(sock); if (!sender) { fprintf(stderr,"strange : no member at %d\n",sock); return 0; } /* 把發送者的姓名添加到消息文本前邊 */ bufrptr = pktbufr; strcpy(bufrptr, sender->name); bufrptr += strlen(bufrptr) + 1; strcpy(bufrptr, text); bufrptr += strlen(bufrptr) + 1; bufrlen = bufrptr - pktbufr; for (memb = group[sender->grid].mems; memb; memb = memb->next) { if (memb->sock == sock) { continue; } /* 給聊天室其餘成員發送消息(TCP是全雙工的) */ sendpkt(memb->sock, USER_TXET, bufrlen, pktbufr); } printf("%s : %s", sender->name, text); return 1; }
/* ChatClient.c */ #include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include<string.h> #include<strings.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<netdb.h> #include<time.h> #include<errno.h> #include"common.h" #define QUIT_STRING "/end" /*打印聊天室名單*/ void showgroups(long lent, char *text) { char *tptr; tptr = text; printf("%15s %15s %15s\n","group","capacity","occupancy"); while (tptr < text + lent) { char *name, *capa, *occu; name = tptr; tptr = name + strlen(name) + 1; capa = tptr; tptr = capa + strlen(capa) + 1; occu = tptr; tptr = occu + strlen(occu) + 1; printf("%15s %15s %15s\n",name, capa, occu); } } /*加入聊天室*/ int joingroup(int sock) { Packet *pkt; char bufr[MAXPKTLENE]; char *bufrptr; int bufrlen; char *gname; char *mname; /*請求聊天信息*/ sendpkt(sock, LIST_GROUPS, 0, NULL); /*接收聊天室信息回覆*/ pkt = recvpkt(sock); if (!pkt) { fprintf(stderr,"error: server died\n"); exit(1); } if (pkt->type != LIST_GROUPS) { fprintf(stderr,"error: unexpected reply from server\n"); exit(1); } /*顯示聊天室*/ showgroups(pkt->lent, pkt->text); /*從標準輸入讀入聊天室名*/ printf("which group? "); fgets(bufr, MAXPKTLENE, stdin); bufr[strlen(bufr) - 1] = '\0'; /*此時可能用戶想退出*/ if (strcmp(bufr, "") == 0 || strncmp(bufr, QUIT_STRING, strlen(QUIT_STRING)) == 0) { close(sock); exit(0); } gname = strdup(bufr); /*讀入成員名字*/ printf("what nickname? "); fgets(bufr, MAXPKTLENE, stdin); bufr[strlen(bufr) - 1] = '\0'; /*此時可能用戶想退出*/ if (strcmp(bufr, "") == 0 || strncmp(bufr, QUIT_STRING, strlen(QUIT_STRING)) == 0) { close(sock); exit(0); } mname = strdup(bufr); /*發送加入聊天室信息*/ bufrptr = bufr; strcpy(bufrptr, gname); bufrptr += strlen(bufrptr) + 1; strcpy(bufrptr, mname); bufrptr += strlen(bufrptr) + 1; bufrlen = bufrptr - bufr; sendpkt(sock, JOIN_GROUP, bufrlen, bufr); /* 讀取來自服務器的回覆 */ pkt = recvpkt(sock); if (!pkt) { fprintf(stderr, "error : server died\n"); exit(1); } if (pkt->type != JOIN_ACCEPTED && pkt->type != JOIN_REJECTED) { fprintf(stderr, "error : unexpected reply from server\n"); exit(1); } /* 若是拒絕顯示其緣由 */ if (pkt->type == JOIN_REJECTED) { printf("admin : %s\n", pkt->text); free(gname); free(mname); return 0; } else { printf("admin: join '%s' as '%s'\n",gname, mname); free(gname); free(mname); return 1; } } int main(int argc, char **argv) { int sock; /* 用戶輸入合法性檢測 */ if (argc != 1) { fprintf(stderr,"usage : %s\n", argv[0]); exit(1); } /* 與服務器鏈接 */ sock = hooktoserver(); if (sock == -1) { exit(1); } /* 清除標準輸出緩衝區 */ fflush(stdout); /* 初始化描述符集 */ fd_set clientfds, tempfds; FD_ZERO(&clientfds); FD_ZERO(&tempfds); FD_SET(sock, &clientfds); /* 設置服務器套接字在clientfds中的比特位 */ FD_SET(0, &clientfds); /* 設置標準輸入在 clientfds 中的比特位 */ while(1) { /* 加入聊天室 */ if (!joingroup(sock)) { continue; } while(1) { /* 調用 select 函數同時監測鍵盤和服務器信息 */ tempfds = clientfds; if (select(FD_SETSIZE, &tempfds, NULL, NULL, NULL) == -1) { perror("select"); exit(4); } /* 對於全部在 tempfds 中被置位的文件描述符,檢測它是不是套接字描述符, 若是是,意味服務器傳來消息。若是它文件描述符是 0,則意味有來自用戶 鍵盤的輸入要發送給服務器 */ /* 處理服務器傳來信息 */ if(FD_ISSET(sock, &tempfds)) { Packet *pkt; pkt = recvpkt(sock); if (!pkt) { fprintf(stderr, "error: server died\n"); exit(1); } /* 顯示消息文本 */ if (pkt->type != USER_TXET) { fprintf(stderr,"error: unexpected reply from server\n"); exit(1); } printf("%s: %s", pkt->text, pkt->text + strlen(pkt->text) + 1); freepkt(pkt); } if (FD_ISSET(0, &tempfds)) { char bufr[MAXPKTLENE]; fgets(bufr, MAXPKTLENE, stdin); /* 處理鍵盤輸入 */ if (strncmp(bufr, QUIT_STRING, strlen(QUIT_STRING)) == 0) { sendpkt(sock, LEAVE_GROUP, 0, NULL); break; } sendpkt(sock, USER_TXET, strlen(bufr) + 1, bufr); } } } }
/* groups */ 4 ind 2 uk 2 us 2 aus 3
/* makefile */ .c.o: gcc -g -c $? # compile client and server all: chatclient chatserver # compile client only chatclient: chatclient.o chatlinker.o gcc -g -o chatclient chatclient.o chatlinker.o # compile server program chatserver: chatserver.o chatlinker.o gcc -g -o chatserver chatserver.o chatlinker.o .PHONY:clean clean: rm chatlinker.o chatclient.o chatserver.o chatclient chatserver
運行結果:緩存