對于網絡TCP/IP的相關知識,這裡不做介紹,隻介紹Linux中如何使用的問題,相關詳細知識參考文檔TCPIP相關知識
1、socket簡介
在 Linux學習(二十):程序間通信 中我們介紹過, 常用的程序間通信方式 1、傳統的程序間通信方式
無名管道(pipe)、有名管道(fifo)和信号(signal)
2、System V IPC對象
共享記憶體(share memory)、消息隊列(message queue)和信号燈(semaphore)
3、BSD
套接字(socket)
套接字通信方式一般用于網絡通信中,也就是在不同的PC之間通信(當然也可以用于本地通信)。
1.1套接字的類型
流式套接字(SOCK_STREAM) 提供了一個面向連接配接、可靠的資料傳輸服務,資料無差錯、無重複的發送且按發送順序接收。内設定流量控制,避免資料流淹沒慢的接收方。資料被看作是位元組流,無長度限制。TCP就屬于此類 資料報套接字(SOCK_DGRAM) 提供無連接配接服務。資料包以獨立資料包的形式被發送,不提供無差錯保證,資料可能丢失或重複,順序發送,可能亂序接收 。UDP屬于此類 原始套接字(SOCK_RAW) 可以對較低層次協定如 IP 、 ICMP 直接通路。
2、IP位址轉換
我們常用的IP位址的形式是“192.168.0.1”這樣的點分十進制,需要轉換成對應的整形資料。
in_addr_t inet_addr(const char *strptr); //将字元串轉化為二進制的值
char*inet_ntoa(stuct in_addr inaddr); //将32位資料轉化位字元串的點分十進制
3、端口号
為了區分一台主機接收到的資料包應該轉交給哪個程序來進行處理,使用端口号來差別 TCP端口号與UDP端口号獨立 端口号一般由IANA(Internet Assigned Numbers Authority) 管理 衆所周知端口: 1~1023 ( 1~255 之間為衆所周知端口, 256~1023 端口通常由 UNIX 系統占用) 已登記端口: 1024~4999 動态或私有端口 : 5000~65535
4、位元組序
不同類型CPU的主機中,記憶體存儲多位元組整數序列有兩種方法,稱為主機位元組序(HBO): 小端序(little-endian)- 低序位元組存儲在低位址 将低位元組存儲在起始位址,稱為“Little-Endian”位元組序,Intel、AMD等采用的是這種方式; 大端序(big-endian)-高序位元組存儲在低位址 将高位元組存儲在起始位址,稱為“Big-Endian”位元組序,由ARM、Motorola等所采用 如何測試目前主機的位元組序呢,參考 資料存儲的大小端 網絡中傳輸的資料必須按網絡位元組序, 即大端位元組序 在大部分PC機上,當應用程序将整數送入socket前,需要轉化成網絡位元組序;當應用程序從socket取出整數後,要轉化成小端位元組序。這是因為主機位元組序可能和網絡位元組序的大小端不同。 主機位元組序與網絡位元組序的轉化有以下幾個函數 主機位元組序到網絡位元組序 u_long htonl (u_long hostlong); u_short htons (u_short short);
網絡位元組序到主機位元組序 u_long ntohl (u_long hostlong); u_short ntohs (u_short short);
5 網絡程式設計相關API
5.1 建立socket
頭檔案:#include <sys/types.h>
#include <sys/socket.h>
函數原型 int socket(int domain, int type, int protocol);
功能:建立一個套接字,傳回一個檔案描述符
參數:
domain:通信域,協定族
AF_UNIX 本地通信
AF_INET 網絡通信
AF_PACKET 底層的協定
type:類型
SOCK_STREAM 流式套接字 tcp
SOCK_DGRAM 資料報套接字 udp
SOCK_RAW 原始套接字
protocol:協定,一般為0
傳回值:
成功:檔案描述符
失敗:-1
例程:
int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("fail to socket");
//return -1;
exit(1);
}
5.2 綁定套接字bind
頭檔案: #include <sys/types.h>
#include <sys/socket.h> 函數原型: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能: 将套接字與網絡資訊結構體綁定 參數: 1、sockfd:檔案描述符,socket的傳回值 2、addr:網絡資訊結構體,通用的格式如下 struct sockaddr {
sa_family_t sa_family; 2個位元組
char sa_data[14]; 14個位元組
} 但通用的這種格式我們一般不使用,因為使用起來不友善,我們一般使用下面這種格式(注意格 式強制轉換): struct sockaddr_in { sa_family_t sin_family; 位址族 AF_INET 2個位元組
in_port_t sin_port; 端口号 2個位元組 struct in_addr sin_addr; } 這個結構體的頭檔案位于#include <netinet/in.h> struct in_addr 的格式如下: struct in_addr
{
in_addr_t s_addr; //IP位址 4個位元組 };
3、addrlen:addr的長度 傳回值:
成功:0
失敗:-1
例程:
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(9999);
serveraddr.sin_addr.s_addr = inet_addr("192.168.2.123");
if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in)) < 0)
{
perror("fail to bind");
exit(1);
}
5.3 監聽套接字listen
頭檔案: #include <sys/types.h>
#include <sys/socket.h>
函數原型 int listen(int sockfd, int backlog);
功能: 将套接字設定為被動監聽模式
參數:
sockfd:檔案描述符,socket的傳回值
backlog:允許同時響應用戶端的連接配接請求的個數,一般為5, 10
傳回值:
成功:0
失敗:-1
例程:
if(listen(sockfd, 5) < 0)
{
perror("fail to listen");
exit(1);
}
5.4 接收套接字accept
頭檔案: #include <sys/types.h>
#include <sys/socket.h>
函數原型: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能: 阻塞等待用戶端的連接配接請求
參數:
sockfd:檔案描述符,socket的傳回值
addr:網絡資訊結構體(被填充的擷取到的用戶端的網絡資訊結構體,同樣使用struct sockaddr_in類 型)
addrlen:addr的長度(注意此處為指針形式,bind中為類整形)
傳回值:
成功:新的檔案描述符(用于通信)
失敗:-1
注意:accept的傳回值是一個新的檔案描述符,這個描述符是用于後面讀寫通信的,原來的檔案描述符用于listen和accept中。
5.5 連接配接套接字connect
頭檔案: #include <sys/types.h>
#include <sys/socket.h>
函數原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能: 發送用戶端的連接配接請求
參數:
sockfd:檔案描述符,socket的傳回值
addr:網絡資訊結構體(自己填充的伺服器的網絡資訊結構體,同樣使用struct sockaddr_in類型)
addrlen:addr的長度
傳回值:
成功:0
失敗:-1
例程:
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
perror("fail to connect");
exit(1);
}
5.6 發送
頭檔案: #include <sys/types.h> #include <sys/socket.h>
函數原型: ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能: 發送資料(用于TCP通信)
參數:
sockfd:檔案描述符
伺服器:accept的傳回值
用戶端:socket的傳回值
buf:發送的資料
len:資料的長度
flags:标志位
0 阻塞
MSG_DONTWAIT 非阻塞
傳回值:
成功:發送的資料的位元組數
失敗:-1
頭檔案: #include <sys/socket.h>
函數原型:ssize_t sendto(int socket, const void *message, size_t length,
int flags, const struct sockaddr *dest_addr, socklen_t dest_len);
功能: 發送資料(用于UDP通信)
參數:
socket:檔案描述符
message:發送的資料
length:資料的長度
flags:标志位,一般為0
dest_addr:目的位址(發送給誰)
dest_len:addr的長度
傳回值:
成功:發送的資料的位元組數
失敗:-1
5.7 讀資料
頭檔案: #include <sys/types.h>
#include <sys/socket.h>
函數原型: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收資料(用于TCP通信)
參數:
sockfd:檔案描述符
伺服器:accept的傳回值
用戶端:socket的傳回值
buf:接收的資料
len:資料的長度
flags:标志位
0 阻塞
MSG_DONTWAIT 非阻塞
傳回值:
成功:接收的資料的位元組數
0 發送端檔案描述符關閉
失敗:-1
注意:在發送端關閉描述符時,接收到接收到0位元組資料,這點要單獨處理一下。
頭檔案: #include <sys/types.h>
#include <sys/socket.h>
函數原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能: 接收資料(用于UDP通信)
參數:
sockfd:檔案描述符
buf:接收的資料
len:資料的長度
flags:标志位,一般為0
src_addr:源的位址(自動填充)
addrlen:addr的長度
傳回值:
成功:接收的資料的位元組數
失敗:-1
6、典型C/S通信流程
典型的C/S通信過程如下
伺服器端:
#include <stdio.h> //printf
#include <arpa/inet.h> //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h> //socket bind listen accept connect
#include <netinet/in.h> //sockaddr_in
#include <stdlib.h> //exit
#include <unistd.h> //close
#include <string.h>
#define N 128
#define errlog(errmsg) do{\
perror(errmsg);\
printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, const char *argv[])
{
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {};
ssize_t bytes;
if(argc < 3)
{
printf("您輸入的參數太少了: %s <ip> <port>\n", argv[0]);
exit(1);
}
//第一步:建立套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
errlog("fail to socket");
}
//第二步:填充伺服器網絡資訊結構體
//inet_addr:将點分十進制ip位址轉化為網絡位元組序的整型資料
//htons:将主機位元組序轉化為網絡位元組序
//atoi:将數字型字元串轉化為整型資料
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字域網絡資訊結構體綁定
if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
errlog("fail to bind");
}
//第四步:将套接字設定為監聽狀态
if(listen(sockfd, 5) < 0)
{
errlog("fail to listen");
}
//第五步:阻塞等待用戶端的連接配接請求
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
{
errlog("fail to accept");
}
//列印用戶端的ip位址、端口号
printf("%s --- %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
while(1)
{
if((bytes = recv(acceptfd, buf, N, 0)) < 0)
{
errlog("fail to recv");
}
else if(bytes == 0)
{
printf("NO DATA\n");
exit(1);
}
else
{
if(strncmp(buf, "quit", 4) == 0)
{
printf("client is quited ...\n");
break;
}
else
{
printf("client : %s\n", buf);
strcat(buf, " *_*");
if(send(acceptfd, buf, N, 0) < 0)
{
errlog("fail to send");
}
}
}
}
close(acceptfd);
close(sockfd);
return 0;
}
用戶端例程
#include <stdio.h> //printf
#include <arpa/inet.h> //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h> //socket bind listen accept connect
#include <netinet/in.h> //sockaddr_in
#include <stdlib.h> //exit
#include <unistd.h> //close
#include <string.h>
#define N 128
#define errlog(errmsg) do{\
perror(errmsg);\
printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, const char *argv[])
{
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {};
if(argc < 3)
{
printf("您輸入的參數太少了: %s <ip> <port>\n", argv[0]);
exit(1);
}
//第一步:建立套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
errlog("fail to socket");
}
//第二步:填充伺服器網絡資訊結構體
//inet_addr:将點分十進制ip位址轉化為網絡位元組序的整型資料
//htons:将主機位元組序轉化為網絡位元組序
//atoi:将數字型字元串轉化為整型資料
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
#if 0
//用戶端也可以自己指定自己的資訊
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
clientaddr.sin_addr.s_addr = inet_addr(argv[3]);
clientaddr.sin_port = htons(atoi(argv[4]));
if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
{
errlog("fail to bind");
}
#endif
//第三步:發送用戶端的連接配接請求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
errlog("fail to connect");
}
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) < 0)
{
errlog("fail to send");
}
if(strncmp(buf, "quit", 4) == 0)
{
printf("client quit ...\n");
break;
}
else
{
if(recv(sockfd, buf, N, 0) < 0)
{
errlog("fail to recv");
}
printf("server : %s\n", buf);
}
}
close(sockfd);
return 0;
}
執行結果: 伺服器
用戶端:
127開頭的IP位址表示主機位址
對于UDP的通信來講就簡單多了,建立了socket之後,用戶端就可以直接發送了(因為伺服器的位址資訊是已知的),伺服器在接收到來自用戶端的資料後,也就有了來自用戶端的位址資訊,服務也就可以向用戶端發送資料了。 伺服器例程:
#include <stdio.h> //printf
#include <arpa/inet.h> //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h> //socket bind listen accept connect
#include <netinet/in.h> //sockaddr_in
#include <stdlib.h> //exit
#include <unistd.h> //close
#include <string.h>
#define N 128
#define errlog(errmsg) do{\
perror(errmsg);\
printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, const char *argv[])
{
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {};
ssize_t bytes;
if(argc < 3)
{
printf("您輸入的參數太少了: %s <ip> <port>\n", argv[0]);
exit(1);
}
//第一步:建立套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
errlog("fail to socket");
}
//第二步:填充伺服器網絡資訊結構體
//inet_addr:将點分十進制ip位址轉化為網絡位元組序的整型資料
//htons:将主機位元組序轉化為網絡位元組序
//atoi:将數字型字元串轉化為整型資料
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字域網絡資訊結構體綁定
if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
errlog("fail to bind");
}
while(1)
{
if((bytes = recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
{
errlog("fail to recvfrom");
}
else if(bytes == 0)
{
printf("NO DATA\n");
exit(1);
}
else
{
//列印用戶端的ip位址、端口号
printf("%s --- %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
if(strncmp(buf, "quit", 4) == 0)
{
printf("client is quited ...\n");
break;
}
else
{
printf("client : %s\n", buf);
strcat(buf, " *_*");
if(sendto(sockfd, buf, N, 0, (struct sockaddr *)&clientaddr, addrlen) < 0)
{
errlog("fail to sendto");
}
}
}
}
close(sockfd);
return 0;
}
用戶端例程:
#include <stdio.h> //printf
#include <arpa/inet.h> //inet_addr htons
#include <sys/types.h>
#include <sys/socket.h> //socket bind listen accept connect
#include <netinet/in.h> //sockaddr_in
#include <stdlib.h> //exit
#include <unistd.h> //close
#include <string.h>
#define N 128
#define errlog(errmsg) do{\
perror(errmsg);\
printf("%s --> %s --> %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, const char *argv[])
{
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {};
if(argc < 3)
{
printf("您輸入的參數太少了: %s <ip> <port>\n", argv[0]);
exit(1);
}
//第一步:建立套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
errlog("fail to socket");
}
//第二步:填充伺服器網絡資訊結構體
//inet_addr:将點分十進制ip位址轉化為網絡位元組序的整型資料
//htons:将主機位元組序轉化為網絡位元組序
//atoi:将數字型字元串轉化為整型資料
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(sendto(sockfd, buf, N, 0, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
errlog("fail to sendto");
}
if(strncmp(buf, "quit", 4) == 0)
{
printf("client quit ...\n");
break;
}
else
{
//if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&serveraddr, &addrlen) < 0)
if(recvfrom(sockfd, buf, N, 0, NULL, NULL) < 0)
{
errlog("fail to recvfrom");
}
printf("server : %s\n", buf);
}
}
close(sockfd);
return 0;
}
執行結果: 伺服器
用戶端