目錄
一、socket相關資料、流程
附:Linux頭檔案整理
附:I/O讀寫操作函數
二、用父子程序實作簡單的網絡聊天程式(C/C++ 運作環境Ubuntu)
client.c
server.c
運作效果
三、pthread實作簡單的網絡聊天程式(C/C++ 運作環境Ubuntu)
pthread建立程序/線程及鎖的相關代碼
思路及流程
server.c
server.c
運作效果
PS:現在以前端的角度寫了個簡易聊天室,github位址:https://github.com/ChenMingK/chat-room
一、socket相關資料、流程

Socket?
網絡上的兩個程式通過一個雙向的通信連接配接實作資料的交換,這個連接配接的一端稱為一個socket。
建立網絡通信連接配接至少要一對端口号(socket)。socket本質是程式設計接口(API),對TCP/IP的封裝,TCP/IP也要提供可供程式員做網絡開發所用的接口,這就是Socket程式設計接口;HTTP是轎車,提供了封裝或者顯示資料的具體形式;Socket是發動機,提供了網絡通信的能力。
Socket的英文原義是“孔”或“插座”。作為BSD UNIX的程序通信機制,取後一種意思。通常也稱作"套接字",用于描述IP位址和端口,是一個通信鍊的句柄,可以用來實作不同虛拟機或不同計算機之間的通信。在Internet上的主機一般運作了多個服務軟體,同時提供幾種服務。每種服務都打開一個Socket,并綁定到一個端口上,不同的端口對應于不同的服務。Socket正如其英文原義那樣,像一個多孔插座。一台主機猶如布滿各種插座的房間,每個插座有一個編号,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟體将插頭插到不同編号的插座,就可以得到不同的服務。
TCP與UDP
TCP---傳輸控制協定,提供的是面向連接配接、可靠的位元組流服務。當客戶和伺服器彼此交換資料前,必須先在雙方之間建立一個TCP連接配接,之後才能傳輸資料。TCP提供逾時重發,丢棄重複資料,檢驗資料,流量控制等功能,保證資料能從一端傳到另一端。
UDP---使用者資料報協定,是一個簡單的面向資料報的運輸層協定。UDP不提供可靠性,它隻是把應用程式傳給IP層的資料報發送出去,但是并不能保證它們能到達目的地。由于UDP在傳輸資料報前不用在客戶和伺服器之間建立一個連接配接,且沒有逾時重發等機制,故而傳輸速度很快
TCP和UDP都是在傳輸層上的。簡單來說,UDP發送資料的時候是不管資料有沒有真正達到目的地的,是以傳輸起來速度就比較快了。但是同時也容易造成資料丢失。而TCP我們知道有三次握手建立,四次握手釋放,是以傳輸更準确,但是速度可能會相對慢一些。
為確定正确地接收資料,TCP要求在目标計算機成功收到資料時發回一個确認(即ACK)。如果在某個時限内未收到相應的ACK,将重新傳送資料包。如果網絡擁塞,這種重新傳送将導緻發送的資料包重複。但是,接收計算機可使用資料包的序号來确定它是否為重複資料包,并在必要時丢棄它。
socket函數建立套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket函數對應于普通檔案的打開操作。普通檔案的打開操作傳回一個檔案描述字,而socket()用于建立一個socket描述符(socket descriptor),它唯一辨別一個socket。這個socket描述字跟檔案描述字一樣,後續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
正如可以給fopen的傳入不同參數值,以打開不同的檔案。建立socket的時候,也可以指定不同的參數建立不同的socket描述符,socket函數的三個參數分别為
domain:即協定域,又稱為協定族(family)。常用的協定族有,AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協定族決定了socket的位址類型,在通信中必須采用對應的位址,如AF_INET決定了要用ipv4位址(32位的)與端口号(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為位址。
type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:故名思意,就是指定協定。常用的協定有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分别對應TCP傳輸協定、UDP傳輸協定、STCP傳輸協定、TIPC傳輸協定。
注意:并不是上面的type和protocol可以随意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的預設協定。
當我們調用socket建立一個socket時,傳回的socket描述字它存在于協定族(address family,AF_XXX)空間中,但沒有一個具體的位址。如果想要給它指派一個位址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動随機配置設定一個端口。
bind函數
bind函數将一個位址族中的特定位址賦給socket,例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6位址和端口号組合賦給socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數的三個參數分别為:
sockfd:即socket描述字,它是通過socket()函數建立的,唯一辨別一個socket。bind()函數就是将給這個描述字綁定一個名字。
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協定位址。這個位址結構根據位址建立socket時的位址協定族的不同而不同,如ipv4對應的是:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
ipv6對應的是:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
Unix域對應的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
addrlen:對應的是位址的長度。
通常伺服器在啟動的時候都會綁定一個衆所周知的位址(如ip位址+端口号),用于提供服務,客戶就可以通過它來接連伺服器;而用戶端就不用指定,有系統自動配置設定一個端口号和自身的ip位址組合。這就是為什麼通常伺服器端在listen之前會調用bind(),而用戶端就不會調用,而是在connect()時由系統随機生成一個。
主機位元組序與網絡位元組序
主機位元組序就是我們平常說的大端和小端模式:不同的CPU有不同的位元組序類型,這些位元組序是指整數在記憶體中儲存的順序,這個叫做主機序。引用标準的Big-Endian和Little-Endian的定義如下:
a) Little-Endian就是低位位元組排放在記憶體的低位址端,高位位元組排放在記憶體的高位址端。
b) Big-Endian就是高位位元組排放在記憶體的低位址端,低位位元組排放在記憶體的高位址端。
網絡位元組序:4個位元組的32 bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然後16~23bit,最後是24~31bit。這種傳輸次序稱作大端位元組序。由于TCP/IP首部中所有的二進制整數在網絡中傳輸時都要求以這種次序,是以它又稱作網絡位元組序。位元組序,顧名思義位元組的順序,就是大于一個位元組類型的資料在記憶體中的存放順序,一個位元組的資料沒有順序的問題了。
是以:在将一個位址綁定到socket的時候,請先将主機位元組序轉換成為網絡位元組序,而不要假定主機位元組序跟網絡位元組序一樣使用的是Big-Endian。
listen(),connect()函數
如果作為一個伺服器,在調用socket()、bind()之後就會調用listen()來監聽這個socket,如果用戶端這時調用connect()發出連接配接請求,伺服器端就會接收到這個請求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數的第一個參數即為要監聽的socket描述字,第二個參數為相應socket可以排隊的最大連接配接個數。socket()函數建立的socket預設是一個主動類型的,listen函數将socket變為被動類型的,等待客戶的連接配接請求。
connect函數的第一個參數即為用戶端的socket描述字,第二參數為伺服器的socket位址,第三個參數為socket位址的長度。用戶端通過調用connect函數來建立與TCP伺服器的連接配接。
accept()函數
TCP伺服器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket位址了。TCP用戶端依次調用socket()、connect()之後就向TCP伺服器發送了一個連接配接請求。TCP伺服器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接配接就建立好了。之後就可以開始網絡I/O操作了,即類同于普通檔案的讀寫I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函數的第一個參數為伺服器的socket描述字,第二個參數為指向struct sockaddr *的指針,用于傳回用戶端的協定位址,第三個參數為協定位址的長度。如果accpet成功,那麼其傳回值是由核心自動生成的一個全新的描述字,代表與傳回客戶的TCP連接配接。
注意:accept的第一個參數為伺服器的socket描述字,是伺服器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數傳回的是已連接配接的socket描述字。一個伺服器通常通常僅僅隻建立一個監聽socket描述字,它在該伺服器的生命周期内一直存在。核心為每個由伺服器程序接受的客戶連接配接建立了一個已連接配接socket描述字,當伺服器完成了對某個客戶的服務,相應的已連接配接socket描述字就被關閉。
close()函數
在伺服器與用戶端建立連接配接之後,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的檔案要調用fclose關閉打開的檔案。
#include <unistd.h>
int close(int fd);
close一個TCP socket的預設行為時把該socket标記為已關閉,然後立即傳回到調用程序。該描述字不能再由調用程序使用,也就是說不能再作為read或write的第一個參數。
注意:close操作隻是使相應socket描述字的引用計數-1,隻有當引用計數為0的時候,才會觸發TCP用戶端向伺服器發送終止連接配接請求。
附:Linux頭檔案整理
sys/types.h:資料類型定義
sys/socket.h:提供socket函數及資料結構
netinet/in.h:定義資料結構sockaddr_in
arpa/inet.h:提供IP位址轉換函數
netdb.h:提供設定及擷取域名的函數
sys/ioctl.h:提供對I/O控制的函數
sys/poll.h:提供socket等待測試機制的函數
其他在網絡程式中常見的頭檔案
unistd.h:提供通用的檔案、目錄、程式及程序操作的函數
errno.h:提供錯誤号errno的定義,用于錯誤處理
fcntl.h:提供對檔案控制的函數
time.h:提供有關時間的函數
crypt.h:提供使用DES加密算法的加密函數
pwd.h:提供對/etc/passwd檔案通路的函數
shadow.h:提供對/etc/shadow檔案通路的函數
pthread.h:提供多線程操作的函數
signal.h:提供對信号操作的函數
sys/wait.h、sys/ipc.h、sys/shm.h:提供程序等待、程序間通訊(IPC)及共享記憶體的函數
附:I/O讀寫操作函數
read() write()
recv() send()
readv() writev()
recvmsg() sendmsg()
recvfrom() sendto()
#include<unistd.h>
1. ssize_t read(int fd, void *buf, size_t count);
2. ssize_t write(int fd, const void *buf, size_t count);
推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上可以把上面的其它函數都替換成這兩個函數
read函數是負責從fd中讀取内容.當讀成功時,read傳回實際所讀的位元組數,如果傳回的值是0表示已經讀到檔案的結束了,小于0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網絡連接配接出了問題。
write函數将buf中的nbytes位元組内容寫入檔案描述符fd.成功時傳回寫的位元組數。失敗時傳回-1,并設定errno變量。 在網絡程式中,當我們向套接字檔案描述符寫時有倆種可能。
1)write的傳回值大于0,表示寫了部分或者是全部的資料。
2)傳回的值小于0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現了中斷錯誤。如果為EPIPE表示網絡連接配接出現了問題(對方已經關閉了連接配接)。
二、用父子程序實作簡單的網絡聊天程式(C/C++ 運作環境Ubuntu)
為什麼需要子程序?
為了實作聊天的功能,用戶端和伺服器都需要一個程序來讀取連接配接,另一個程序來處理鍵盤輸入。
以伺服器為例,如果隻有一個程序,它隻能接收用戶端的輸入資訊,之後再回報給用戶端,如何能在接收資訊的同時又向用戶端輸入資訊呢?那麼就需要一個子程序來write
流程圖及說明
伺服器端首先建立套接字(socket),然後定義一個位址結構并将套接字綁定到位址上(bind),接着進行監聽(listen)直到監聽到用戶端,接着建立一個子程序用于接受鍵盤輸入并發送資訊給用戶端,同時父程序接受子程序發送的資料流并列印。如果用戶端關閉,父程序利用信号處理函數殺死子程序。最終關閉伺服器和用戶端的套接字。
用戶端程式首先建立套接字,然後嘗試與伺服器端連接配接(不用綁定到位址結構),成功連接配接則建立一個子程序用于接收用戶端發送的資料流并列印,父程序用于接收鍵盤輸入并發送資料流給伺服器端。輸入CTRL+C則子程序中調用信号處理函數殺死父程序。
代碼及運作效果(照着别人的改的僅供參考...建議用Java提供的socket的API來實作)
client.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#define CLTIP "127.0.0.1"
#define SRVPORT 10005
void handler()
{
exit(0);
}
int main()
{
/*建立一個套接字*/
int clientsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(clientsock < 0)
{
printf("socket creation failed\n");
exit(-1);
}
printf("socket create successfully.\n");
/*定義一個位址結構*/
struct sockaddr_in clientAddr;
clientAddr.sin_family = AF_INET;
clientAddr.sin_port = htons((u_short)SRVPORT);
clientAddr.sin_addr.s_addr = inet_addr(CLTIP);
/*進行連接配接*/
if(connect(clientsock, (struct sockaddr*)&clientAddr, sizeof(struct sockaddr)) < 0)
{
printf("Connect error.IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
exit(-1);
}
printf("Connect to IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
pid_t pid ;
pid = fork();
if(pid == -1)
exit(0);
if(pid == 0) //子程序複制接收資料并顯示出來
{
char recvbuf[1024]={0};
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = read(clientsock ,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
exit(0);
}
if(ret == 0) //連接配接關閉
{
printf("the server has closed\n");
kill(getppid(),SIGUSR1);
break;
}
else
{
printf("Get message from server:");
fputs(recvbuf,stdout);
}
}
}
else //父程序負責從鍵盤接收輸入并發送
{
signal(SIGUSR1,handler);
char sendbuf[1024]={0} ;
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
write(clientsock,sendbuf,strlen(sendbuf));
memset(&sendbuf,0,sizeof(sendbuf));
}
}
close(clientsock);
return 0;
}
server.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#define SRVIP "127.0.0.1"
#define SRVPORT 10005
/*信号處理函數*/
void handler(int sig)
{
exit(0);
}
int main()
{
/* 建立一個套接字*/
int serversocket= socket(AF_INET ,SOCK_STREAM,IPPROTO_TCP);
if(serversocket < 0)
exit(0);
/*定義一個位址結構并填充*/
struct sockaddr_in serverAddr;
serverAddr.sin_family=AF_INET;
serverAddr.sin_port = htons((u_short)SRVPORT);
serverAddr.sin_addr.s_addr = inet_addr(SRVIP);
/*将套接字綁定到位址上*/
if(bind(serversocket, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr))==-1)
{
printf("Bind error.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
exit(0);
}
printf("Bind successful.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
/*監聽套接字,成為被動套接字*/
if(listen(serversocket,10)==-1)
{
printf("Listen error!\n");
exit(0);
}
printf("Listening on port[%d]\n", serverAddr.sin_port);
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn ;
conn = accept(serversocket,(struct sockaddr*)&peeraddr,&peerlen);
if(conn <0)
exit(0);
else
printf("new client touch.\n");
pid_t pid ;
pid = fork();//建立一個新程序
if(pid == -1)
{
exit(0);
}
if(pid == 0)//子程序
{
signal(SIGUSR1,handler); //注冊使用者定義信号1 鍵盤輸入CTRL+C執行handler函數
char sendbuf[1024] = {0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
write(conn,sendbuf,sizeof(sendbuf));
memset(&sendbuf,0,sizeof(sendbuf));
}
exit(0);
}
else //父程序 用來擷取資料
{
char recvbuf [1024]={0};
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = read(conn ,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
exit(0);
}
if(ret == 0) //對方已關閉
{
printf("對方關閉\n");
break;
}
fputs(recvbuf,stdout);
}
kill(pid,SIGUSR1); //發送信号,執行子程序中的handler函數(殺死子程序)
exit(0);
}
/*關閉套接字*/
close(serversocket);
close(conn);
return 0;
}
運作效果
亂碼是因為列印一些中文資訊(懶得改了)
三、pthread實作簡單的網絡聊天程式(C/C++ 運作環境Ubuntu)
pthread建立程序/線程及鎖的相關代碼
pthread_t:線程ID
pthread_attr_t:線程屬性
pthread_create(&tid,&attr,function,parameter):建立一個線程
pthread_exit(NULL):終止目前線程
pthread_cancel():中斷另外一個線程的運作
pthread_join(tid,NULL):等待線程編号為tid的線程運作到結束(這裡不加&)
pthread_attr_init(&attr):初始化線程的屬性
pthread_attr_setdetachstate():設定脫離狀态的屬性(決定這個線程在終止時是否可以被結合)
pthread_attr_getdetachstate():擷取脫離狀态的屬性
pthread_attr_destroy():删除線程的屬性
pthread_kill():向線程發送一個信号
pthread_mutex_init() 初始化互斥鎖
pthread_mutex_destroy() 删除互斥鎖
pthread_mutex_lock():占有互斥鎖(阻塞操作)
pthread_mutex_trylock():試圖占有互斥鎖(不阻塞操作)。即,當互斥鎖空閑時,将占有該鎖;否則,立即傳回。
pthread_mutex_unlock(): 釋放互斥鎖
pthread_cond_init():初始化條件變量
pthread_cond_destroy():銷毀條件變量
pthread_cond_signal(): 喚醒第一個調用pthread_cond_wait()而進入睡眠的線程
pthread_cond_wait(): 等待條件變量的特殊條件發生
Thread-local storage(或者以Pthreads術語,稱作線程特有資料):
pthread_key_create(): 配置設定用于辨別程序中線程特定資料的鍵
pthread_setspecific(): 為指定線程特定資料鍵設定線程特定綁定
pthread_getspecific(): 擷取調用線程的鍵綁定,并将該綁定存儲在 value 指向的位置中
pthread_key_delete(): 銷毀現有線程特定資料鍵
pthread_attr_getschedparam();擷取線程優先級
pthread_attr_setschedparam();設定線程優先級
思路及流程
在上一回的父程序子程序的基礎上改進,取消父子程序改為多線程,C/S連接配接流程不再說明。簡單介紹下代碼中用到的函數
server.c中的void *run為伺服器每連接配接一個用戶端新建立一個線程時該線程所要做的工作,首先要把該用戶端在伺服器中對應的socket套接字(int值)作為參數傳遞進來,然後循環調用read函數接收即可,如果用戶端關閉,調用pthread_exit(NULL)殺死該線程。
server.c中的void *sendmessage和client.c中的該函數的功能類似,都是接收鍵盤輸入的資料并發送出去,且都作為一個線程的運作函數(server.c中在accept之前建立該線程)。server.c中是向所有用戶端發送,是以需要一個全局的數組來存儲已建立的用戶端的socket套接字,循環調用write函數即可;而client.c中就沒有這麼麻煩,隻有其自己的套接字,一個while循環即可。
下面再簡單介紹下用到的pthread.h中的函數及資料結構
pthread_t tid; //線程辨別符
pthread_attr_t; //線程屬性
pthread_attr_init() //設定線程屬性
pthread_create() //線程建立,參數依次為線程辨別,屬性,函數名稱,函數參數
其中線程屬性可用NULL設定為預設,函數參數沒有則用NULL
如果有多個參數用一個結構體打包(隻能傳遞void *指針)
且注意要在函數中進行指針類型轉換再擷取指針内容
pthread_join(tid,NULL) //等待線程辨別為tid的線程結束
pthread_exit() //線程結束,參數一般設為0
server.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#include<pthread.h>
#define CLTIP "127.0.0.1"
#define SRVPORT 10005
void *sendmessage(void *arg)
{
char sendbuf[1024]={0} ;
int clientsock = *((int *) arg);
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
write(clientsock,sendbuf,strlen(sendbuf));
memset(&sendbuf,0,sizeof(sendbuf));
}
close(clientsock);
exit(0);
}
//父線程負責接收資料,子線程接收鍵盤輸入并發送資料給伺服器
int main()
{
/*建立一個套接字*/
int clientsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(clientsock < 0)
{
printf("socket creation failed\n");
exit(-1);
}
printf("socket create successfully.\n");
/*定義一個位址結構*/
struct sockaddr_in clientAddr;
clientAddr.sin_family = AF_INET;
clientAddr.sin_port = htons((u_short)SRVPORT);
clientAddr.sin_addr.s_addr = inet_addr(CLTIP);
/*進行連接配接*/
if(connect(clientsock, (struct sockaddr*)&clientAddr, sizeof(struct sockaddr)) < 0)
{
printf("Connect error.IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
exit(-1);
}
printf("Connect to IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
pthread_t tid;
pthread_create(&tid,NULL,sendmessage,&clientsock); //子線程接受鍵盤輸入并發送資料
char recvbuf[1024]={0};
int ret;
while(1) //父線程接收伺服器發送的資料
{
memset(recvbuf,0,sizeof(recvbuf));
ret = read(clientsock ,recvbuf,sizeof(recvbuf));
if (ret == -1 || ret == 0)
break;
printf("Get message from server:");
fputs(recvbuf,stdout);
}
pthread_join(tid,NULL);
return 0;
}
server.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#include<pthread.h>
#define SRVIP "127.0.0.1"
#define SRVPORT 10005
int client_fd[10] = {-1};
int num_of_client = 0;
int flag = 0;
void *run(void *arg)
{
int clientsocket = *((int *) arg); //取内容 先強制轉換
char recvbuf[1024] = {0};
while(1) //對應用戶端關閉則結束線程
{
int ret = read(clientsocket,recvbuf,sizeof(recvbuf));
if(ret == 0) //對方關閉
{
printf("The client %d has closed\n",clientsocket);
flag = 1;
break;
}
printf("Get messages from client %d :",clientsocket);
fputs(recvbuf,stdout);
}
close(clientsocket);
pthread_exit(NULL);
}
void *sendmessage(void *arg)
{
char sendbuf[1024] ;
memset(sendbuf,0,sizeof(sendbuf));
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
for(int i=0;i<10;i++)
{
if(client_fd[i] != 0)
{
write(client_fd[i],sendbuf,sizeof(sendbuf));
}
}
memset(&sendbuf,0,sizeof(sendbuf));
}
pthread_exit(NULL);
}
int main()
{
/* 建立一個套接字*/
int serversocket= socket(AF_INET ,SOCK_STREAM,IPPROTO_TCP);
if(serversocket < 0)
exit(0);
/*定義一個位址結構并填充*/
struct sockaddr_in serverAddr;
serverAddr.sin_family=AF_INET;
serverAddr.sin_port = htons((u_short)SRVPORT);
serverAddr.sin_addr.s_addr = inet_addr(SRVIP);
/*将套接字綁定到位址上*/
if(bind(serversocket, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr))==-1)
{
printf("Bind error.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
exit(0);
}
printf("Bind successful.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
/*監聽套接字,成為被動套接字*/
if(listen(serversocket,10)==-1)
{
printf("Listen error!\n");
exit(0);
}
printf("Listening on port[%d]\n", serverAddr.sin_port);
struct sockaddr_in clientaddr; //用于accept傳回協定位址
socklen_t clientaddr_len = sizeof(clientaddr_len);
int *conn_fd;
pthread_t send_message_tid;
pthread_create(&send_message_tid,NULL,sendmessage,NULL);
while(1) //收發資訊都在建立的線程中
{
conn_fd = (int*) malloc(sizeof(int));
*conn_fd = accept(serversocket,NULL,NULL); //傳回新的套接字,注意服務結束後要關閉
if(*conn_fd != -1)
{
printf("accept a client \n");
client_fd[num_of_client++] = *conn_fd;
}
pthread_t tid;
pthread_create(&tid,NULL,run,conn_fd);
//不等待...
}
/*關閉套接字*/
close(serversocket);
return 0;
}
運作效果
上面為最終運作效果,這裡稍加解釋:首先運作伺服器,每監聽到一個用戶端則輸出”accept a client”,以上accept了2個用戶端。伺服器可以向所有的用戶端發送資訊,如”hello everyone”
每個用戶端發送的資訊會發送給伺服器(這裡沒有實作用戶端群聊),上圖中伺服器會顯示從哪個socket接收了資料。
收工......