一、需求定義
本課程設計是在Linux環境下基於Socket進行開發的。因爲之前也學過計算機網絡原理,接觸過TCP/UDP 這些東西,所以就想試着寫一個聊天室的功能出來。通過之前的學習,我知道了系統服務器和客戶瑞組成。服務端程序通過共享存儲區存儲聊天數據,併發送給每個連接的客產端。通過多路複用的子進程實現服務端與多個客戶端之間的數據發送與接收。可以在單機上開將兩個該口分別運行客戶、服務器的程序。經linux下gcc調試成功,可以實現簡單的羣聊效果。關鍵詞:網絡聊天,linux ,socket.相關概念及技術
實現語言:C語言
二、分析設計
- 服務器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。
- 然後客戶端初始化一個Socket,然後連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。
- 客戶端通過提示,輸入用戶名,start(),方法回顯是否成功加入聊天室(最多支持五十個客戶端)。
- 客戶端發送數據請求,服務器端接收請求並處理請求,然後把迴應數據發送給客戶端,客戶端讀取數據(注:我是服務器和客戶端通過 send() 和 recv() 進行通信)。
- 最後關閉連接,一次交互結束。
- 服務器讀取信息。
- 客戶端關閉。
- 服務器端關閉。
三、代碼實現和測試
1.開發環境
VmWare+Centos7.5
-
客戶端的代碼(源碼)
#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 <pthread.h>
#include <time.h>
int clientfd2;//客戶端socket
char* IP = "127.0.0.1";//服務器的IP
short PORT = 6666;//服務器服務端口
typedef struct sockaddr meng;
char name[30];//設置支持的用戶名長度
time_t nowtime;
void init(){
clientfd2 = socket(PF_INET,SOCK_STREAM,0);//創建套接字
struct sockaddr_in addr;//將套接字存在sockaddr_in結構體中
addr.sin_family = PF_INET;//地址族
addr.sin_port = htons(PORT);//端口號 可隨意設置,不過不可超過規定的範圍
addr.sin_addr.s_addr = inet_addr(IP);//inet_addr()函數將點分十進制的字符串轉換爲32位的網絡字節順序的ip信息
//發起連接
if (connect(clientfd2,(meng*)&addr,sizeof(addr)) == -1){
perror("無法連接到服務器");
exit(-1);
}
printf("客戶端啓動成功\n");
}
void start(){
pthread_t id;
void* recv_thread(void*);
//創建一個線程用於數據的接收,一個用於數據的發送
pthread_create(&id,0,recv_thread,0);
char buf2[100] = {};
sprintf(buf2,"%s進入了羣聊",name);
time(&nowtime);
printf("進入的時間是: %s\n",ctime(&nowtime));
send(clientfd2,buf2,strlen(buf2),0);
while(1){
char buf[100] = {};
scanf("%s",buf);
char msg[100] = {};
sprintf(msg,"%s發送的信息是:%s",name,buf);
send(clientfd2,msg,strlen(msg),0);
if (strcmp(buf,"quit") == 0){
memset(buf2,0,sizeof(buf2));//初始化
sprintf(buf2,"%s退出了羣聊",name);
send(clientfd2,buf2,strlen(buf2),0);
break;
}
}
close(clientfd2);
}
void* recv_thread(void* p){
while(1){
char buf[100] = {};
if (recv(clientfd2,buf,sizeof(buf),0) <= 0){
break;
}
printf("%s\n",buf);
}
}
int main(){
init();
printf("請輸入用戶名:");
scanf("%s",name);
printf("\n\n*****************************\n");
printf("歡迎%s 進入羣聊\n",name);
printf(" 輸入quit 退出\n");
printf("\n*****************************\n\n");
start();
return 0;
}
-
服務器代碼(源碼)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <time.h>
int serverfd;//服務器socket
int clientfd[100];//客戶端的socketfd,100個元素,clientfd[0]~clientfd[99]
int size =50;//用來控制進入聊天室的人數爲50以內
char* IP = "127.0.0.1";//主機ip地址
short PORT = 6666;//端口號
typedef struct sockaddr meng;
time_t nowtime;
void init(){
serverfd = socket(PF_INET,SOCK_STREAM,0);
if (serverfd == -1){
perror("創建socket失敗");
exit(-1);
}
//爲套接字設置ip協議 設置端口號 並自動獲取本機ip轉化爲網絡ip
struct sockaddr_in addr;//存儲套接字的信息
addr.sin_family = PF_INET;//地址族
addr.sin_port = htons(PORT);//設置server端端口號,你可以隨便設置,當sin_port = 0時,系統隨機選擇一個未被使用的端口號
addr.sin_addr.s_addr = inet_addr(IP);//把127.0.0.1改爲自己的server端的ip地址,當sin_addr = INADDR_ANY時,表示從本機的任一網卡接收數據
//綁定套接字
if (bind(serverfd,(meng*)&addr,sizeof(addr)) == -1){
perror("綁定失敗");
exit(-1);
}
if (listen(serverfd,100) == -1){//監聽最大連接數
perror("設置監聽失敗");
exit(-1);
}
}
void SendAll(char* msg){
int i;
for (i = 0;i < size;i++){
if (clientfd[i] != 0){
printf("發送給%d\n",clientfd[i]);
printf("發送的信息是: %s\n",msg);
//寫入文件
char buf[1024];
FILE *logs = fopen("log.txt", "a+");
if(logs== NULL)
{
printf("open file erroe: \n");
}else{
sprintf(buf, "進入時間:%s\tIP地址:%s\n",ctime(&nowtime),IP);
fputs(buf,logs);
sprintf(buf, "所發信息:%s\n",msg);
fputs(buf,logs);
fclose(logs);
}
send(clientfd[i],msg,strlen(msg),0);
}
}
}
void* server_thread(void* p){
int fd = *(int*)p;
printf("pthread = %d\n",fd);
while(1){
char buf[100] = {};
if (recv(fd,buf,sizeof(buf),0) <= 0){
int i;
for (i = 0;i < size;i++){
if (fd == clientfd[i]){
clientfd[i] = 0;
break;
}
}
printf("退出:fd = %d 退出了。\n",fd);
char buf[1024];
FILE *logs = fopen("log.txt", "a");
if(logs== NULL)
{
printf("open file erroe: \n");
}else{
sprintf(buf, "退出時間:%s\tIP地址:%s\n",ctime(&nowtime),IP);
fputs(buf,logs);
fclose(logs);
}
pthread_exit(0);
}
//把服務器接受到的信息發給所有的客戶端
SendAll(buf);
}
}
void server(){
printf("服務器啓動\n");
while(1){
struct sockaddr_in fromaddr;
socklen_t len = sizeof(fromaddr);
int fd = accept(serverfd,(meng*)&fromaddr,&len);
//調用accept進入堵塞狀態,等待客戶端的連接
if (fd == -1){
printf("客戶端連接出錯...\n");
continue;
}
int i = 0;
for (i = 0;i < size;i++){
if (clientfd[i] == 0){
//記錄客戶端的socket
clientfd[i] = fd;
printf("線程號= %d\n",fd);//
//有客戶端連接之後,啓動線程給此客戶服務
pthread_t tid;
pthread_create(&tid,0,server_thread,&fd);
break;
}
if (size == i){
//發送給客戶端說聊天室滿了
char* str = "對不起,聊天室已經滿了!";
send(fd,str,strlen(str),0);
close(fd);
}
}
}
}
int main(){
init();
server();
}
4.實現原理及過程
客戶端過程:
客戶端的過程比較簡單,創建 Socket,連接服務器,將 Socket 與遠程主機連接(注意:只有 TCP 纔有“連接”的概念,一些 Socket 比如 UDP、ICMP 和 ARP 沒有“連接”的概念),發送數據,讀取響應數據,直到數據交換完畢,關閉連接,結束 TCP 對話。
服務端過程:
- 咱再來聊聊服務端的過程,服務端先初始化 Socket,建立流式套接字,與本機地址及端口進行綁定,然後通知 TCP,準備好接收連接,調用
accept()
阻塞,等待來自客戶端的連接。如果這時客戶端與服務器建立了連接,客戶端發送數據請求,服務器接收請求並處理請求,然後把響應數據發送給客戶端,客戶端讀取數據,直到數據交換完畢。最後關閉連接,交互結束。 - 在服務端啓動後,調用
accept()
時,Socket 會進入waiting狀態。客戶端請求連接時,方法建立連接並返回服務器。accept()
返回一個含有兩個元素的元組 (conn, addr)。第一個元素 conn 是新的 Socket 對象,服務器必須通過它與客戶端通信;第二個元素 addr 是客戶端的 IP 地址及端口。 - 接下來是處理階段,服務器和客戶端通過
send()
和recv()
通信(傳輸數據)。
服務器調用send()
,並採用字符串形式向客戶端發送信息,send()
返回已發送的字符個數。
服務器調用recv()
從客戶端接收信息。調用recv()
時,服務器必須指定一個整數,它對應於可通過本次方法調用來接收的最大數據量。recv()
在接收數據時會進入blocked狀態,最後返回一個字符串,用它表示收到的數據。如果發送的數據量超過了recv()
所允許的,數據會被截短。多餘的數據將緩衝於接收端,以後調用recv()
時,會繼續讀剩餘的字節,如果有多餘的數據會從緩衝區刪除(以及自上次調用recv()
以來,客戶端可能發送的其它任何數據)。傳輸結束,服務器調用 Socket 的close()
關閉連接。 -
TCP 三次握手的 Socket 過程:
- 服務器調用
socket()
、bind()
、listen()
完成初始化後,調用accept()
阻塞等待; - 客戶端 Socket 對象調用
connect()
向服務器發送了一個 SYN 並阻塞; - 服務器完成了第一次握手,即發送 SYN 和 ACK 應答;
- 客戶端收到服務端發送的應答之後,從
connect()
返回,再發送一個 ACK 給服務器; - 服務器 Socket 對象接收客戶端第三次握手 ACK 確認,此時服務端從
accept()
返回,建立連接
5.TCP 四次揮手的 Socket 過程:
- 某個應用進程調用
close()
主動關閉,發送一個 FIN; - 另一端接收到 FIN 後被動執行關閉,併發送 ACK 確認;
- 之後被動執行關閉的應用進程調用
close()
關閉 Socket,並也發送一個 FIN; - 接收到這個 FIN 的一端向另一端 ACK 確認。
5.運行截圖
圖 1 啓動服務端
圖 2 服務端成功啓動
圖 3 啓動客戶端
圖 4 客戶端輸入用戶名
圖 5 啓動兩個客戶端
圖 6 羣聊實現
注:上面的代碼如果直接拷入可能無法使用(字符編碼問題)