(一) CS模型
也就是TCP的客戶/服務器模型,我們這裏用一個簡單的回射客戶/服務器模型來進行模擬驗證。客戶發送數據,服務器接收到數據,並將數據原封不動的返回給客戶端。
(二)用到的函數
1.socket()函數
1)函數原型:
int socket(int domain, int type, int protocol);
2)功能:
創建一個套接字用於通信。
3)參數:
1.domain:即協議域,又稱爲協議族(family):
2.type:指定socket類型:常用的socket類型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(數據包套接字)。
3.protocol:公共協議;
常用的公共協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議。
注意:上面的type和protocol不是可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol爲0時,會自動選擇type類型對應的默認協議。
4.返回值:成功返回非負,失敗返回-1。
5.errno
函數socket()並不總是執行成功,有可能會出現錯誤,錯誤的產生有多種原因,可以通過errno獲得:
2.bind()函數
1)作用:將特定的ip地址,port端口號綁定到socket上;bind()函數把一個地址族中的特定地址賦給socket。如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。將socket與你本機上的一個端口相關聯(往往當你在設計服務器端程序時需要調用該函數。隨後你就可以在該端口監聽服務請求;而客戶端一般無須調用該函數)。
2)原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3)參數:
1.sockfd:即socket描述字:
它是通過socket()函數創建了,唯一標識一個socket。
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址,包含有關你的地址的信息:名稱、端口和IP 地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同。
如ipv4對應的是:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr; /* internet address */
//通常設定設定sin_addr爲INADDR_ANY(表示任意的意思)
};
/* Internet address. */
/*sin_addr結構體中只有一個唯一的字段s_addr,表示IP地址,該字段是一個整數,一般用函數inet_addr()把字符串形式的IP地址轉換成unsigned long型的整數值後再置給s_addr。*/
struct in_addr {
uint32_t s_addr;
};
可以用下面的賦值實現自動獲得本機IP地址和隨機獲取一個沒有被佔用的端口號:
my_addr.sin_port = 0; /* 系統隨機選擇一個未被使用端口號 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本IP地址 */
注意:
1.服務程序在爲其socket綁定IP地址時可以把htonl(INADDR_ANY)置給s_addr,這樣做的好處是不論哪個網段上的客戶程序都能與該服務程序通信;
2.如果只給運行在多宿主機上的服務程序的socket綁定一個固定的IP地址,那麼就只有與該IP地址處於同一個網段上的客戶程序才能與該服務程序通信。
ipv6對應的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
Unix域對應的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
3)addrlen:對應的是地址的長度。
4)通常服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲什麼通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
5)返回值:
bind()函數在成功被調用時返回0,當bind()函數調用錯誤的時候,它也是返回–1 作爲錯誤發生的標誌。errn 的值爲錯誤代碼。小於1024 的所有端口都是保留下來作爲系統使用端口的,沒有root 權利無法使用。你可以使用1024 以上的任何端口,一直到65535。調用bind()的常見錯誤是EADDRINUSE,即指定的地址正在使用,主要是指定的端口號被使用了,IP地址可以被多個進程使用,但端口在同一時刻只能被一個進程使用。
6)bind範例:
下面是一個bind函數調用的例子:
struct sockaddr_in saddr;
memset((void *)&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
//saddr.sin_addr.s_addr = inet_addr("192.168.22.5"); 綁定固定IP
bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));
3.網絡字節序 與 主機字節序
(1)主機字節序:主機字節序就是我們平常說的大端和小端模式:不同的CPU有不同的字節序類型。這些字節序是指整數在內存中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:
a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。如32位的十六進制數0x12345678存儲在4個字節的內存中爲:78存儲在第一個字節,56存儲在第二個字節,34爲第三個字節,12爲第四個字節。
b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。如32位的十六進制數0x12345678存儲在4個字節的內存中爲:12存儲在第一個字節,34存儲在第二個字節,56爲第三個字節,78爲第四個字節。
注意:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序一樣使用的是Big-Endian。由於這個問題曾引發過血案!公司項目代碼中由於存在這個問題,導致了很多莫名其妙的問題,所以請謹記對主機字節序不要做任何假定,務必將其轉化爲網絡字節序再賦給socket。
htonl():
1)原型:
2)背景:計算機數據存儲有兩種字節優先順序:高位字節優先和低位字節優先。Internet上數據以高位字節優先順序在網絡上傳輸,所以對於在內部是以低位字節優先方式存儲數據的機器,在Internet上傳輸數據時就需要進行轉換。
幾個字節順序轉換函數:
htons()--"Host to Network Short" ; htonl()--"Host to Network Long"
ntohs()--"Network to Host Short" ; ntohl()--"Network to Host Long"
4.listen()函數
1)原型:
2)作用:調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
3)參數:
sockfd 是一個套接字描述符,爲要監聽的socket描述字。
backlog相應socket可以排隊的最大連接個數。backlog 具體一些是什麼意思呢?每一個連入請求都要進入一個連入請求隊列,等待listen 的程序調用accept()(accept()函數下面有介紹)函數來接受這個連接。當系統還沒有調用accept()函數的時候,如果有很多連接,那麼本地能夠等待的最大數目就是backlog 的數值。
4)返回值:
listen()如果返回 –1 ,那麼說明在listen()的執行過程中發生了錯誤。
5)注意:
socket()函數創建的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的連接請求。
5.connect()函數
1)原型:
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
2)參數:
sockfd :套接字文件描述符,由socket()函數返回的,此中爲客戶端的sockfd。
serv_addr 是一個存儲遠程計算機的IP 地址和端口信息的結構,一般爲服務器的ip與port的結構。
addrlen 應該是sizeof(struct sockaddr)。
3)作用:
客戶端通過調用connect函數來建立與TCP服務器的連接。
6.accept()函數
過程1:TCP服務器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之後就想TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。之後就可以開始網絡I/O操作了,即類同於普通文件的讀寫I/O操作。
過程2:
1.有人從很遠很遠的地方嘗試調用connect()來連接你的機器上的某個端口(當然是你已經在listen()的)。
2.他的連接將被listen 加入等待隊列等待accept()函數的調用(加入等待隊列的最多數目由調用listen()函數的第二個參數backlog 來決定)。
3.你調用accept()函數,告訴他你準備連接。accept函數將回返回一個新的套接字描述符,這個描述符就代表了這個連接!
1)原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2)參數:
sockfd :爲服務器的socket描述字;
addr:存儲着遠程連接過來的計算機的信息(比如遠程計算機的IP 地址和端口)。用於返回客戶端的協議地址。
addrlen:sizeof(struct sockaddr_in)
3)返回值:
如果accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。
4)注意:
1.accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字.
2.accept函數返回的是已連接的socket描述字.
3.一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命週期內一直存在。
服務器端代碼&&客戶端代碼
服務器端代碼
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
int listenfd;
/*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
ERR_EXIT("socket");
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(5188);//端口,主機轉網絡
/*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN)<0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
int conn;//已連接套接字
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept");
char recvbuf[1024];
while(1){
memset(recvbuf,0,sizeof(recvbuf));//初始化recvbuf
int ret=read(conn,recvbuf,sizeof(recvbuf));//函數從打開的文件,設備中讀取數據,返回讀取的字節數。
fputs(recvbuf,stdout);
write(conn,recvbuf,ret);//buf中數據被複制到了TCP發送緩衝區
}
close(conn);
close(listenfd);
return 0;
}
客戶端代碼
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
int sock;
/*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
ERR_EXIT("socket");
//IPV4地址結構
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//地址家族
servaddr.sin_port=htons(5188);//端口,主機轉網絡
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("connect");
char sendbuf[1024]={0};
char recvbuf[1024]={0};
while(fgets(sendbuf,sizeof(sendbuf),stdin) !=NULL){
write(sock,sendbuf,strlen(sendbuf));//發送
read(sock,recvbuf,sizeof(recvbuf));//接受
fputs(recvbuf,stdout);
memset(sendbuf,0,sizeof(sendbuf));
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock);
return 0;
}
“`