3、【網路程式設計】Socket程式設計
一、Socket定義
Socket:在TCP/IP協議中,“IP地址+TCP或UDP埠號”唯 一標識網路通訊中的一個程序,所以“IP地址+埠號”就稱為socket。 在TCP協議中,建立連線的兩個程序各自有一個socket來標識,那麼這兩個socket組成的socket pair就唯一標識一個連線。 TCP/IP協議最早在BSD UNIX上實現,為TCP/IP協議設計的應用層程式設計介面稱為socket API。
二、TCP套接字程式設計模型
1、伺服器端流程簡:
(1)建立套接字(socket);
(2)將套接字繫結到一個本地地址和埠上(bind);
(3)將套接字設定為監聽模式,準備接受客戶端請求(listen);
(4)阻塞等待客戶端請求到來。當請求到來後,接受連線請求,返回一個新的對應於此客戶端連線的套接字sockClient(accept);
(5)用返回的套接字sockClient和客戶端進行通訊(send/recv);
(6)返回,等待另一個客戶端請求(accept);
(7)關閉套接字(close);
2、客戶端流程:
(1) 建立套接字(socket);
(2) 向伺服器發出連線請求(connect);
(3) 和伺服器進行通訊(send/recv);
(4) 關閉套接字(close);
具體流程如下圖所示:
三、Socket基本操作
1、建立套接字,socket函式
int socket(int domain, int type, int protocol)
//成功時返回檔案控制代碼,失敗時返回-1.
//domain 套接字中使用的協議族資訊
//type 套接字資料傳輸型別資訊
//protocol 計算機間通訊使用的協議資訊
(1)domain所選的協議族
名稱 | 協議族 |
---|---|
PF_INET | IPV4網際網路協議族 |
PF_INET6 | IPV6網際網路協議族 |
(2)type套接字型別
type型別 | 作用 |
---|---|
SOCK_STREAM | 面向連線的套接字 |
SOCK_DGRAM | 面向訊息的套接字 |
(3)protacol協議的最終選擇
同一協議中存在多個數據型別傳輸方式相同的協議,就通過protocol區分最終協議,如果只有一個,預設為0。
2、分配IP和埠,bind函式
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen)
//成功返回0,失敗返回-1
//sockfd 套接字檔案描述符
//myaddr 結構體變數地址值,包括IP地址和埠號
//addrlen 結構體變數的長度
bind()函式把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和埠號組合賦給socket。
(1)sockfd:即socket描述字,它是通過socket()函式建立的,唯一標識一個socket。bind()函式就是將給這個描述字繫結一個名字。
(2)addr:一個const struct sockaddr *指標,指向要繫結給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不同而不同,如IPV4對應的是:
struct sockaddr_in {
/*address family:AF_INET*/
sa_family_t sin_family;
/*port in network byte order */
in_port_t sin_port;
/* internet address */
struct in_addr sin_addr;
};
/* Internet address. */
struct in_addr {
/* address in network byte order */
uint32_t s_addr;
};
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 */
};
(3)addrlen:對應的是地址的長度。
通常伺服器在啟動的時候都會繫結一個眾所周知的地址(如ip地址+埠號),用於提供服務,客戶就可以通過它來接連伺服器;而客戶端就不用指定,有系統自動分配一個埠號和自身的ip地址組合。這就是為什麼通常伺服器端在listen之前會呼叫bind(),而客戶端就不會呼叫,而是在connect()時由系統隨機生成一個。
3、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伺服器的連線。
4、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描述字就被關閉。
5、read()/write()函式
萬事具備只欠東風,至此伺服器與客戶已經建立好連線了。可以呼叫網路I/O進行讀寫操作了,即實現了網咯中不同程序之間的通訊!網路I/O操作有下面幾組:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
開發語言不同可能讀寫函式也就不同,只要把自己想要傳送的訊息,以位元組流的方式寫入Socket或者從Socket讀出來即可實現網路的I/O操作。
6、close()函式
在伺服器與客戶端建立連線之後,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完開啟的檔案要呼叫fclose關閉開啟的檔案。
#include <unistd.h>
int close(int fd);
close一個TCP socket的預設行為時把該socket標記為以關閉,然後立即返回到呼叫程序。該描述字不能再由呼叫程序使用,也就是說不能再作為read或write的第一個引數。
注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向伺服器傳送終止連線請求。
三、Socket程式設計例項
咋Linux上實現的一個簡單的socket通訊例項:
Server端:
#include<stdio.h>
//下面兩個標頭檔案是使用socket必須引入的
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//啟動伺服器通訊埠
int startup(int _port,const char* _ip)
{
//socket()函式開啟一個網路通訊視窗,成功則返回一個檔案描述符,應用程式可以向讀寫檔案一樣用read/write在網路上轉發資料。
//若調用出錯則返回-1
//socket()函式的三個引數:協議型別, 套接字型別, 協議型別的常量或設定為0
//AF_INET(IPv4協議) SOCK_STREAM位元組流套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;//網路程式設計中常用的資料結構
local.sin_family=AF_INET;//IPVC4地址族
local.sin_port=htons(_port);//將埠地址轉換為網路二進位制數字
local.sin_addr.s_addr=inet_addr(_ip);//將網路地址轉換為網路二進位制數字
socklen_t len=sizeof(local);
//繫結套接字:成功返回0, 失敗返回-1
//功能:將sock和local繫結在一起,使得sock這個用於網路通訊的問價描述符監聽local所描述的地址和埠
if(bind(sock,(struct sockaddr*)&local,len)<0)
{
perror("bind");
exit(2);
}
//listen(int sockfd, int backlog)監聽函式,sockfd為要監聽的socket套接字,backlog為可以排隊的最大連線數。
//socket()函式建立的socket預設是一個主動型別的,listen函式將socket變為被動型別的,等待客戶的連線請求。
//監聽成功返回0, 失敗返回-1
if(listen(sock,5)<0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage: [local_ip] [local_port]",argv[0]);
return 3;
}
//啟動伺服器套接字listen_socket
int listen_socket=startup(atoi(argv[2]),argv[1]);
struct sockaddr_in remote;
socklen_t len=sizeof(struct sockaddr_in);
while(1)
{
//accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)函式,
//sockfd為伺服器的socket套接字,addr為客戶端協議地址,addrlen為協議地址的長度,
//如果accept成功,則返回一個由核心自動生成的全新套接字,代表與返回客戶的TCP連線
int socket=accept(listen_socket,(struct sockaddr*)&remote,&len);
if(socket<0)
{
perror("accept");
continue;
}
//inet_ntoa:將網路二進位制數字轉換為網路地址
//ntohs:將網路二進位制數字轉換為埠號
printf("client,ip:%s,port:%d\n",inet_ntoa(remote.sin_addr)\
,ntohs(remote.sin_port));
char buf[1024];
while(1)
{
//呼叫網路I/O進行讀寫
ssize_t _s=read(socket,buf,sizeof(buf)-1);
if(_s>0)
{
buf[_s]=0;
printf("client# %s\n",buf);
}
else
{
printf("client is quit!\n");
break;
}
}
//關閉套接字
close(socket);
}
return 0;
}
Client端:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void usage(const char* proc)
{
printf("usage:%s [ip] [port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 3;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
//sockfd為要監聽的socket套接字,addr引數為伺服器的socket地址,addrlen為socket地址的長度。
//客戶端通過呼叫connect函式來建立與TCP伺服器的連線。
if(connect(sock,(struct sockaddr*)&server,(socklen_t)sizeof(server))<0)
{
perror("connect");
exit(2);
}
char buf[1024];
while(1)
{
printf("send#");
fflush(stdout);
ssize_t _s=read(0,buf,sizeof(buf)-1);
buf[_s-1]=0;
write(sock,buf,_s);
}
close(sock);
return 0;
}