C語言中socket使用講解。
1.socket定義。
在Linux中的網路程式設計是通過socket介面來進行的。其實socket介面也是一種特殊的I/O(在《深入理解計算機系統》這本書中的IO部分也有提到網路也是一種特殊的IO),它也是一種檔案描述符。socket也有一個類似於開啟檔案的函式呼叫,該函式返回一個整型的socket描述符,隨後的建立連線、資料傳輸等操作都是通過socket來實現的。
2.socket的型別。
1 .流式socket(SOCK_STREAM)
流式套接字使用的是TCP協議,由於TCP協議建立在三次握手的基礎上,所以這種型別能夠提供可靠的、面向連線的通訊流,能夠保證資料傳輸的正確性和順序性。
2.資料報socket(SOCK_DGRAM)
資料報套接字使用的是UDP協議,由於UDP將資料扔出去之後就不管的桀驁特性,所以該型別定義了一種無連線的服務,資料通過相互獨立的報文進行傳輸,是無序的,並且不保證是可靠、無差錯的。
3.原始socket
原始套接字允許對底層協議如IP或ICMP(在網路層,而TCP和UDP都在傳輸層)進行直接訪問,功能比較強大但是使用不便,主要用於一些協議的開發。
3.儲存socket資訊的結構體。
在介紹socket函式的使用之前還需要介紹一下儲存socket資訊的結構體。在C語言中有兩個重要的struct資料型別:sockaddr和sockaddr_in,這兩個結構體都是用用來儲存socket的相關資訊的。
struct sockaddr {
unsigned short sa_family;//地址族,2位元組
char sa_data[14];//14位元組的協議地址,包含該socket的IP地址和埠號,14位元組
}
struct sockaddr_in {
short sa_family;//地址族,2位元組
unsigned short int sin_port;//埠號,2位元組
struct in_addr sin_addr;//IP地址,4位元組
unsigned char sin_zero[];//填充0以保持與struct sockaddr同樣大小,8位元組
}
在sockaddr_in中使用了in_addr的結構體:
struct in_addr {
in_addr_t s_addr;
};
表示一個32位的IPv4地址。
in_addr_t一般為32位的unsigned int,其位元組順序為網路位元組序,即該無符號數採用大端位元組序。其中每8位表示一個IP地址中的一個數值。
struct sockaddr是通用的套接字地址,而struct sockaddr_in則是internet環境下套接字的地址形式。
這兩種結構體都是16位元組,並且都存在family屬性,不同的是:sockaddr使用的是14位元組的sa_data,而sockaddr_in將sa_data的14位元組拆分成了sin_port和sin_addr以及sin_zero。
所以這兩種資料包含的內容都是一樣的,明顯sockaddr_in型別將地址和埠拆開更為方便(sockaddr給作業系統使用),所以一般使用sockaddr_in進行填充然後將其轉換為sockaddr。
有人注意到這兩種結構體都存在sa_family欄位,sa_family可選的常見值定義在#include < netinet/in.h >檔案下,其中可選的值有以下幾種:
- AF_INET:IPv4協議
- AF_INET4:IPv6協議
- AF_LOCAL:UNIX域協議
- AF_LINK:鏈路地址協議
- AF_KEY:祕鑰套接字
4.地址轉換。
其中的重點部分:
IP地址其實有三種不同的表示格式:
1)Ascii(網路點分字串)
2)網路地址(32位無符號整形,網路位元組序,大頭)
3)主機地址 (主機位元組序)
在Socket程式設計開發中,通過函式inet_addr和inet_ntoa可以實現點分字串與網路位元組順序格式IP地址之間的轉換。
在Socket程式設計中,有四個函式來完成主機位元組順序格式和網路位元組順序格式之間的轉換,它們是:htonl、htons、ntohl、和ntohs。 htons和ntohs完成16位無符號數的相互轉換,htonl和ntohl完成32位無符號數的相互轉換。
在Linux中有一些函式可以實現主機名和地址的轉換,最為常見的有gethostbyname、gethostbyaddr、getaddrinfo等,它們都可以實現IPv4和IPv6的地址和主機名之間的轉換。
本人對這地址之間的關係也是一頭霧水,所以不做過多解釋。
5.相關函式。
先看一下使用TCP和UDP的流程圖:
TCP:
UDP:
函式的作用顧名思義,使用socket建立一個socket連結,然後使用sockaddr_in進行初始化以儲存所建立的socket資訊。然後使用bind函式將IP地址和埠號進行繫結(用於TCP連結,UDP無需)。然後使用listen開啟監聽,在伺服器使用accept等待連結,客戶端使用connect進行連結等等。
socket:
標頭檔案:#include < sys/socket.h>
函式原型:int socket(int family,int type,int protocol)
引數含義:
family:對應的就是AF_INET、AF_INET6等。
type:套接字型別:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW。
protocol:0
返回值:
成功:非負套接字描述符。
出錯:-1bind:
標頭檔案:#include< sys/socket.h>
函式原型:int bind(int sockfd,struct sockaddr *my_addr,int addrlen)
引數含義:
sockfd:套接字描述符。
my_addr:本地地址。
addrlen:地址長度:
返回值:
成功:0
出錯:-1listen:
標頭檔案:#include < sys/socket.h>
函式原型:int listen(int sockfd,int backlog)
引數含義:
sockfd:套接字描述符
backlog:請求佇列中允許的最大請求數,大多數系統預設為20
返回值:
成功:0
出錯:-1accept:
標頭檔案:#include < sys/socket.h>
函式原型:int accept(int sockfd,struct sockaddr * addr,socklen_t* addrlen)
引數含義:
sockfd:套接字描述符
addr:客戶端地址
addrlen:地址長度
返回值:
成功:0
出錯:-1connect:
標頭檔案:#include < sys/socket.h>
函式原型:int connect(int sockfd,struct sockaddr* serv_addr,int addrlen)
引數含義:
sockfd:套接字描述符
serv_addr:伺服器端地址
addrlen:地址長度
返回值:
成功:0
出錯:-1send
標頭檔案:#include < sys/socket.h>
函式原型:int send(int sockfd,const void* msg,int len,int flags)
引數含義:
sockfd:套接字描述符
msg:指向要傳送資料的指標
len:資料長度
flags:一般為0
返回值:
成功:傳送的位元組數
出錯:-1recv
標頭檔案:#include < sys/socket.h>
函式原型:int recv(int sockfd,void* buf,int len,unsigned int flags)
引數含義:
sockfd:套接字描述符
buf:存放接受資料的緩衝區
len:資料長度
flags:一般為0
返回值:
成功:接受的位元組數
出錯:-1sendto
標頭檔案:#include < sys/socket.h>
函式原型:int sendto(int sockfd,const void* msg,int len,unsigned int flags,const struct sockaddr* to,int tolen)
引數含義:
sockfd:套接字描述符
msg:指向要傳送資料的指標
len:資料長度
flags:一般為0
to:目標機的IP地址和埠號資訊
tolen:地址長度
返回值:
成功:傳送的位元組數
出錯:-1recvfrom
標頭檔案:#include < sys/socket.h>
函式原型:int recvfrom(int sockfd,void * buf,int len,unsigned int flags,struct sockaddr* from,int* fromlen)
引數含義:
sockfd:套接字描述符
buf:存放接收資料的緩衝區
len:資料長度
flags:一般為0
from:源機的IP地址和埠號資訊
tolen:地址長度
返回值:
成功:接收的位元組數
出錯:-1
6.例項操作。
一個簡單的例項:客戶端將“hello”傳給伺服器。
server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#define SERVPORT 3333//定義埠號
#define BACKLOG 10//請求佇列中允許的最大請求數
#define MAXDATASIZE 5//資料長度
int main() {
struct sockaddr_in server_sockaddr,client_sockaddr;//宣告伺服器和客戶端的socket儲存結構
int sin_size,recvbytes;
int sockfd,client_fd;//socket描述符
char buf[MAXDATASIZE];//傳輸的資料
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {//建立socket連結
perror("Socket");
exit(1);
}
printf("Socket success!,sockfd=%d\n",sockfd);
//以sockaddt_in結構體填充socket資訊
server_sockaddr.sin_family = AF_INET;//IPv4
server_sockaddr.sin_port = htons(SERVPORT);//埠
server_sockaddr.sin_addr.s_addr = INADDR_ANY;//本主機的任意IP都可以使用
bzero(&(server_sockaddr.sin_zero),8);//填充0
if((bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))) == -1) {//bind函式繫結
perror("bind");
exit(-1);
}
printf("bind success!\n");
if(listen(sockfd,BACKLOG) == -1) {//監聽
perror("listen");
exit(1);
}
printf("listening ... \n");
if((client_fd = accept(sockfd,(struct sockaddr *) &client_sockaddr,&sin_size)) == -1) {//等待客戶端連結
perror("accept");
exit(1);
}
printf("accept success!\n");
if((recvbytes = recv(client_fd,buf,MAXDATASIZE,0)) == -1) {//接收客戶端的請求
perror("recv");
exit(1);
}
printf("received a connection : %s\n",buf);
close(sockfd);
}
client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <netinet/in.h>
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char *argv[]) {
int sockfd,sendbytes;
char buf[MAXDATASIZE];
struct hostent* host;
struct sockaddr_in serv_addr;
if(argc < 2) {//需要使用者指定連結的地址
fprintf(stderr,"Please enter the server's hostname");
exit(1);
}
if((host = gethostbyname(argv[1])) == NULL) {//轉換為hostent
perror("gethostbyname");
exit(1);
}
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {//建立socket
perror("socket");
exit(1);
}
//填充資料
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if((connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))) == -1) {//發起對伺服器的連結
perror("connect");
exit(1);
}
if((sendbytes = send(sockfd,"hello",5,0)) == -1) {//傳送訊息給伺服器端
perror("send");
exit(1);
}
close(sockfd);
}
執行結果:先執行server開啟監聽等待連結,
再執行client。