作者:李春港
出處:https://www.cnblogs.com/lcgbk/p/14779410.html
- 前言
- (一). 回顧系統編程進程的通信方式
- (二). 網絡編程大綱
- (三). 網絡體系模型結構
- (四). 網絡編程重要概念socket、htons()、htonl()
- (五). TCP協議socket()、struct sockaddr_in、htons()、htonl()、socklen_t、bind()、listen()、accept()、recv()、connect()、send()
- (六).UDP協議recvfrom()、inet_pton()、sendto()
- (七).四種IO模型
- (八).設置屬性函數setsockopt()
- (九).網絡超時接收select、alarm、setsockopt
- (十).廣播、組播setsockopt
前言
本文章主要是講解Linux平台的網絡通信,涉及的深度不是很深,但是覆蓋的范圍比較廣,若需要學習更深的知識點,可以根據本文章涉及到的知識去深度挖去網絡的資源學習下。
(一). 回顧系統編程進程的通信方式
無名管道,有名管道,信號,消息隊列,共享內存,信號量 ---> 在同一個linux系統下
套接字通信 --> 跨主機
主機A 主機A
Rose.c ---- Jack.c ---> 無名管道,有名管道,信號,消息隊列,共享內存,信號量
主機A ---- 主機B
Rose.c Jack.c ---> 套接字通信
(二). 網絡編程大綱
1. 網絡編程傳輸層協議 TCP / UDP
2. 關於網絡概念知識 -- IP,端口號,字節序,socket
3. 網絡通信4種IO模型 -- 阻塞,非阻塞,多路復用,信號驅動
4. 網絡超時接收數據3種方式 -- alarm鬧鍾,多路復用,設置套接字的屬性
5. 網絡廣播,組播 -- 基於UDP協議,組播組的IP分類,如何加入組?
(1)、 網絡編程效果:
系統編程:自己Ubuntu --- 自己Ubuntu
網絡編程: 條件: Ubuntu與開發板之間必須是在相同的網段中,網絡是相通!
自己Ubuntu --- 自己Ubuntu
自己Ubuntu --- 自己開發板
自己Ubuntu --- 別人Ubuntu
自己Ubuntu --- 別人開發板
自己開發板 --- 別人Ubuntu
自己開發板 --- 別人開發板
(2)、 協議:在不同主機之間通信,雙方都必須遵循一個原則。
Apanet協議: 不能互聯不同類型的計算機與不同操作系統的兩台主機
TCP/IP協議: 傳輸控制協議/因特網互聯協議
TCP協議: 用於檢測網絡中差錯。
IP協議: 負責在不同網絡中進行通信。
TCP/IP協議
主機A ----------> 主機B
192.168.0.2 192.168.0.5
傳輸層: TCP協議 TCP協議 --> 一旦發生差錯,就會馬上重新傳輸,直到數據安全到達對方為止!
網絡層: IP協議 IP協議 --> 分析IP地址
(三). 網絡體系模型結構
1.所謂網絡體系結構,指的是主機內部集成的結構與每層協議的集合,每台主機內部都會有這個模型。
2. 網絡模型種類:
1)OSI模型(舊): 7層
現實例子:
"hello"
老板發話 ---> 助理幫老板寫信 ---> 前台幫助理寄信 --> 郵局職員送信 --> 郵局分地區職員
---> 職員選擇正確路線出發 --> 選擇正確交通工具
OSI模型:
-------------------用戶層-------------------
應用層: 老板發話
表示層: 助理幫老板寫信
會話層: 前台幫助理寄信
-------------------內核層-------------------
傳輸層: 郵局職員送信
網絡層: 郵局分地區職員 廣州/珠海 IP地址
-------------------驅動層-------------------
數據鏈路層: 職員選擇正確路線出發 有線網卡/無線網卡
物理層: 選擇正確交通工具 網口,網線
由於OSI模型處理數據效率非常低,這個模型已經被TCP/IP協議所取代
2)TCP/IP協議(新): 4層
現實例子:
老板自己想,自己寫信,自己寄信 ---> 郵局職員送信
---> 郵局分地區職員 --> 職員選擇路線馬上出發
TCP/IP協議模型:
-------------------用戶層-------------------
應用層: 老板自己想,自己寫信,自己寄信
-------------------內核層-------------------
傳輸層: 郵局職員送信
網絡層: 郵局分地區職員 廣州/珠海 IP地址
-------------------驅動層-------------------
網絡接口與物理層: 職員選擇路線馬上出發
3. 頭數據 --> 每經過模型的一層,都會添加/刪除一個頭數據
例題: 現在主機A發送消息給主機B,簡述工作原理。
(四). 網絡編程重要概念socket、htons()、htonl()
1. socket ---> 插座,套接字, 插座類型繁多,就像協議一樣,必須在通信設置好協議。
-
1) socket本身是一個函數接口,作用: 創建套接字
-
2) 無論TCP協議,還是UDP協議,都是使用socket函數去創建 int fd = socket(TCP協議); fd就是TCP套接字 --> 套接字文件 int fd = socket(UDP協議); fd就是UDP套接字 int fd = open("xxx"); --> 普通文件
-
3) 套接字是一種特殊的文件描述符 --> 都是可以用read/write
-
4) 在TCP/IP協議模型,socket處於應用層與傳輸層之間
2. IP地址
-
1)每一個主機內部系統只能有一個IP地址與之對應
-
2)IP地址 --> 32位
-
3)數據包中必須含有目標IP地址,源IP地址。
-
4)常常以點分式"192.168.0.102"
-
5)網絡字節序是大端字節序
3. 端口號 --> 16位 0~65535
Jack.c ---> Rose.c
IP地址: 192.168.0.2 192.168.0.10 --> 要求相同局域網
端口號: 50002 50002
端口號:
1) 系統占用端口號: 0~1023
2) 用戶可用: 1024~65535
4. 字節序 h: host 本地字節序 to: 轉換 n: net 網絡字節序 l: 32位數據 s: 16位數據
htonl() --> 轉IP地址
htons() --> 轉端口號
原則: 不管是服務器還是客戶端,統一在傳輸時把本地字節序轉換為網絡字節序
(五). TCP協議socket()、struct sockaddr_in、htons()、htonl()、socklen_t、bind()、listen()、accept()、recv()、connect()、send()
傳輸層協議:TCP協議(打電話)面向於有連接的通信方式
例題: 使用網絡通信TCP協議,實現不同主機之間的通信
主機A---主機B
Jack.c Rose.c
核心代碼 Rose.c 服務器
-
創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
-
准備好服務器IP地址,端口號,協議 --> 通通塞到結構體中struct sockaddr_in
struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //網際協議 srvaddr.sin_port = htons(atoi(argv[1])); //端口號,atoi是把字符串轉換成整型數 /* 之所以需要這些函數是因為計算機數據表示存在兩種字節順序:NBO與HBO 網絡字節順序NBO(Network Byte Order): 按從高到低的順序存儲,在網絡上使用統一的網絡字節順序,可以避免兼容性問題。 主機字節順序(HBO,Host Byte Order): 不同的機器HBO不相同,與CPU設計有關,數據的順序是由cpu決定的,而與操作系統無關。 如 Intelx86結構下,short型數0x1234表示為34 12, int型數0x12345678表示為78 56 34 12如IBM power PC結構下,short型數0x1234表示為12 34, int型數0x12345678表示為12 34 56 78。 由於這個原因不同體系結構的機器之間無法通信,所以要轉換成一種約定的數序,也就是網絡字 節順序,其實就是如同powerpc那樣的順序 。在PC開發中有ntohl和htonl函數可以用來進行網絡字節和主機字節的轉換。 */ srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址,由於服務器上可能有多個網卡,也就有多個ip地址,所以該ip地址的選項為INADDR_ANY,表示:在本服務器上無論是哪個ip地址接收到數據,只要是這個端口號,服務器都會處理。 */ INADDR_ANY --> 接收任何地址的數據信息 /* Address to accept any incoming messages. */ #define INADDR_ANY ((unsigned long int) 0x00000000)
-
把地址綁定到未連接套接字上
socklen_t就是struct sockaddr_in大小的數據類型 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
-
設置監聽套接字
listen(fd,4); 是fd的本身從未連接套接字轉換監聽套接字 /* backlog參數就是控制我們的已連接隊列里等待accept()取走的連接的最大數目的.注意一點,backlog與這個已排隊連接的最大數目未必是完全相等的,不同的系統的實現可能不同.比如backlog=1,系統允許的實際一排隊數目可能為2. */
-
坐等對方的連接
int connfd = accept(fd,(strutc sockaddr*)&cliaddr,&len);
-
暢聊
recv(connfd,buf,sizeof(buf),0);
-
斷開連接
close(connfd); close(fd);
核心代碼 Jack.c 客戶端
-
創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
-
發起連接
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);
-
暢聊
send(fd,buf,strlen(buf),0);
運行步驟:
同桌Ubuntu 你的Ubuntu
Rose.c Jack.c
192.168.0.10 192.168.0.20
50001 50001
同桌: ping 192.168.0.20
你: ping 192.168.0.10
同桌: ./Rose 50001
你: ./Jack 192.168.0.10 50001
例子1:
Jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
Rose.ccp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
int main(int argc,char *argv[]) // ./Rose 50001
{
//1. 創建一個未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//6. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
//7. 掛斷電話
close(connfd);
close(fd);
return 0;
}
例子2:tcp_chat
Jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void *routine(void *arg)
{
int fd = *(int *)arg;
char buf[50];
while(1)
{
bzero(buf,50);
recv(fd,buf,sizeof(buf),0);
printf("from Rose:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
pthread_t tid;
pthread_create(&tid,NULL,routine,(void *)&fd);
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
Rose.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void *routine(void *arg)
{
int connfd = *(int *)arg;
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(connfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./Rose 50001
{
//1. 創建一個未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//5.5 創建線程,用於實現服務器寫功能
pthread_t tid;
pthread_create(&tid,NULL,routine,(void *)&connfd);
//6. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
//7. 掛斷電話
close(connfd);
close(fd);
return 0;
}
(六).UDP協議recvfrom()、inet_pton()、sendto()
1. UDP協議 user data protrol 用戶數據協議特點:
TCP: 面向連接 --> 一定雙方連接上了才能進行通信!
UDP: 面向非連接 --> 不需要連接就可以進行數據的收發,提高效率。
UDP例子: 寫信
2. UDP實現過程
例題: 客戶端發送數據給服務器,使用UDP完成。
服務器:(收信) Rose.c
(1). 買一個信箱
int fd = socket(AF_INET,SOCK_DGRAM,0);
(2). 綁定一個地址到信箱
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址
bind(fd,(struct sockaddr *)&srvaddr,len);
(3). 不斷收信
recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);
(4). 銷毀信箱
close(fd);
客戶端:(寫信)
(1). 買一個信箱
int fd = socket(AF_INET,SOCK_DGRAM,0);
(2). 准備服務器地址
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
(3). 不斷往服務器地址寫信
sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
(4). 銷毀信箱
close(fd);
例子:
Jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 192.168.0.243 50002
{
//1. 創建UDP套接字(沒有地址的信箱)
int fd = socket(AF_INET,SOCK_DGRAM,0);
//2. 准備服務器的地址
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//AF_INET: 協議,與socket第一個參數一致
//argv[1]: 代表一個字符串,"192.168.0.243"
//&srvaddr.sin_addr: 代表struct in_addr *類型,使用srvaddr變量訪問sin_addr這個變量,再取地址就變成指針了!
/*
struct sockaddr_in
{
u_short sin_family; // 地址族
u_short sin_port; // 端口
struct in_addr sin_addr; // IPV4地址
char sin_zero[8];
};
*/
//3. 不斷寫信
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
if(strncmp(buf,"quit",4) == 0)
break;
}
//4. 回收套接字資源
close(fd);
return 0;
}
Rose.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Rose 50001
{
//1. 創建UDP套接字(沒有地址的信箱)
int fd = socket(AF_INET,SOCK_DGRAM,0);
//2. 准備服務器的IP地址(准備地址)
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址
//3. 綁定地址到套接字(把准備好的地址綁定到信箱上)
bind(fd,(struct sockaddr *)&srvaddr,len);
//4. 不斷從UDP套接字中接收數據
struct sockaddr_in cliaddr;
char buf[50];
while(1)
{
bzero(buf,50);
//不斷從fd這個信箱上讀取cliaddr這個客戶端給我發來的內容,然后存放在buf中
recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);
printf("from Jack:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 關閉套接字資源
close(fd);
return 0;
}
(七).四種IO模型
IO模型: 當信號到達時,程序如何處理這些數據?
方式: 阻塞,非阻塞,多路復用,信號驅動
四種IO模型特性:
1)阻塞IO
1.系統默認得到的文件描述符都是阻塞的
read(fd) recv(fd) recvfrom(fd); --> 這些函數本身不具有阻塞屬性,而是這個文件描述符的本身具有阻塞的屬性導致函數看起來好像阻塞一樣!
2.由於socket套接字是特殊文件描述符,默認創建的套接字都是阻塞的!
2)非阻塞IO
1.給文件描述符添加非阻塞的屬性 --> 缺點: 占用CPU資源較大,負荷大!
2.當非阻塞時,如果沒有數據到達,那么讀取數據就會失敗,一定要不斷詢問套接字/文件描述符中是否有數據的到達!
3)多路復用
1.同時對多個IO口進行操作
2. 可以在規定的時間內檢測數據是否到達 --> 超時知識
4)信號驅動
1.屬於異步通信 --> 一定要給套接字/文件描述符設置信號觸發模式屬性
2. 在套接字/文件描述符有數據到達時,通過發送信號給用戶,用戶就知道有數據到達!
1、非阻塞IO,fcntl()
(1). 阻塞IO與非阻塞IO之間差異?
阻塞IO
建立套接字(默認是阻塞的) ---> 想讀取套接字中數據 --> 判斷緩沖區有沒有數據?
--> 沒有 --> 進入無限等待的狀態 --> 直到緩沖區數據為止 --> 讀取數據 --> 沒有 --> 進入無限等待的狀態
--> 有 --> 讀取數據 --> --> 沒有 --> 進入無限等待的狀態
非阻塞IO
建立套接字(默認是阻塞的) --> 添加非阻塞屬性到套接字上 --> 想讀取套接字中數據 --> 判斷緩沖區有沒有數據?
--> 沒有 --> 讀取失敗 ---> 接口馬上返回,不會一直阻塞 --> 要是想再次讀取,那么就要放在循環中
--> 有 --> 讀取成功 ---> 接口也會返回
(2). 如何給套接字/文件描述符設置非阻塞屬性? --- fcntl() --- man 2 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fd: 需要設置屬性的文件描述符
cmd: 請求控制文件描述符的命令字 非阻塞的屬性
arg: 這個參數要不要填,取決於cmd
cmd:
F_GETFL (void)
Get the file access mode and the file status flags; arg is
ignored. //獲取文件的模式權限標志位,arg可以忽略了。
F_SETFL (long)
Set the file status flags to the value specified by arg. File
access mode (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags
(i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored.
//以上提到的屬性,不能通過fcntl()設置屬性
On Linux this command can change only the
O_APPEND, 文件追加屬性
O_ASYNC, 信號觸發模式
O_DIRECT, 不使用緩沖區寫入
O_NOATIME, 不更新文件的修改時間
and O_NONBLOCK 非阻塞屬性
flags.
注意: 在添加屬性時,所有的屬性使用 "|" 位或來計算
can be bitwise-or'd in flags.
返回值:
成功:
F_GETFL Value of file status flags.
F_SETFL 0
失敗: -1
例子1:直接把文件描述符屬性設置為非阻塞
fd = open("xxx");
fcntl(fd,F_SETFL,O_NONBLOCK);
例子2:創建一個套接字,在套接字原來的屬性的基礎上添加非阻塞屬性。
int fd = socket(xxx);
int state = fcntl(fd,F_GETFL); //獲取文件原來的屬性
state |= O_NONBLOCK; //在原來的基礎上添加非阻塞的屬性
fcntl(fd,F_SETFL,state); //設置state屬性到套接字上
例子3:給TCP通信設置非阻塞屬性
Jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
Rose.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char *argv[]) // ./Rose 50001
{
//1. 創建一個未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//5.5. 設置非阻塞屬性到connfd
int state = fcntl(connfd,F_GETFL);
state |= O_NONBLOCK;
fcntl(connfd,F_SETFL,state);
//6. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s\n",buf);
//usleep(100000);
if(strncmp(buf,"quit",4) == 0)
break;
}
//7. 掛斷電話
close(connfd);
close(fd);
return 0;
}
例子3:寫一個服務器,實現全部連接到該服務器的用戶存放在鏈表中,可實現群發,私聊,客戶端退出等功能。
以“:”形式區別群發內容與私聊內容
例如:hello就是群發
103:hello就是給端口為103的用戶發送hello的消息
提示: strstr() 可以判斷某個字符串內是否有某個字符 :
使用方法 char *strstr(char *str1, char *str2); 意義為 判斷str2是否為str1的子串,若是則返回str2在str1中首次出現的指針位置,若不是返回NULL;
atoi()只會判斷數字,即遇到非數字就會停止轉化。
itoa():將整型值轉換為字符串。
例子: int a = atoi(“103:hello”) --> 只會把103轉為int型,不會理會后面的字符串:hello
Jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void *routine(void *arg)
{
int fd = *(int *)arg;
char buf[50];
while(1)
{
bzero(buf,50);
recv(fd,buf,sizeof(buf),0);
printf("from Rose:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
pthread_t tid;
pthread_create(&tid,NULL,routine,(void *)&fd);
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
server.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
#include <pthread.h>
#include "kernel_list.h"
#include <malloc.h>
//初始化鎖變量
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
struct client *head = NULL;
//設計內核鏈表節點
struct client{
int connfd; //數據域
struct list_head list; //指針域
};
struct client *init_head(struct client *head)
{
head = (struct client *)malloc(sizeof(struct client));
INIT_LIST_HEAD(&(head->list));
return head;
}
int msg_broadcast(char *msg,struct client *sender) //hello
{
struct client *p = NULL;
pthread_mutex_lock(&m);
//p: 遍歷鏈表的指針
//&(head->list): 頭節點指針域的地址
list_for_each_entry(p,&(head->list),list)
{
//除了發送者自己,其他人的都要收到該消息
if(p->connfd == sender->connfd)
{
continue;
}
send(p->connfd,msg,strlen(msg),0);
}
pthread_mutex_unlock(&m);
return 0;
}
int msg_send(int receive_connfd,char *msg)
{
struct client *p = NULL;
pthread_mutex_lock(&m);
list_for_each_entry(p,&(head->list),list)
{
//找到那個私聊的人了
if(p->connfd == receive_connfd)
{
send(p->connfd,msg,strlen(msg),0);
pthread_mutex_unlock(&m);//找到了解鎖
return 0;//找到了就不用繼續找了,提前退出!
}
}
pthread_mutex_unlock(&m);//找不到解鎖
return -1;
}
void *routine(void *arg)
{
struct client* peer = (struct client *)arg;
char msg[200];
//各個線程只需要負責不斷讀取對應的客戶端的數據
while(1)
{
bzero(msg,200);
read(peer->connfd,msg,sizeof(msg));
printf("msg = %s",msg);
//1. 客戶端退出
if(strncmp(msg,"quit",4) == 0)
{
close(peer->connfd);
list_del(&(peer->list));
free(peer);
break;
}
//2. 群發 沒有:
char *tmp = NULL;
tmp = strstr(msg,":");
if(tmp == NULL)
{
msg_broadcast(msg,peer);
}
//3. 私聊 有 5:hello
else{
int receive_connfd = atoi(msg);//5
if(msg_send(receive_connfd,tmp+1) == -1)
{
printf("NOT FOUNT client!\n");
}
}
}
}
int main(int argc,char *argv[])
{
//1. 初始化鏈表頭
head = init_head(head);
//2. 創建TCP套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
//3. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//4. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(sockfd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//5. 將未連接套接字轉換為監聽套接字
listen(sockfd,4);
//6. 不斷等待客戶端連接到服務器中,只要連接上,就尾插到鏈表的末尾!
struct sockaddr_in cliaddr;
int connfd;
while(1)
{
bzero(&cliaddr,len);
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
printf("connfd = %d\n",connfd);
printf("new connection:%s\n",(char *)inet_ntoa(cliaddr.sin_addr));
struct client *new = (struct client *)malloc(sizeof(struct client));
if(new != NULL)
{
//如果新建的節點申請空間成功,那么就進行賦值
new->connfd = connfd;
}
//尾插這個節點到鏈表的末尾
//只要修改鏈表的長度,以及訪問該鏈表,都要上鎖
pthread_mutex_lock(&m);
list_add_tail(&(new->list),&(head->list));
pthread_mutex_unlock(&m);
//只要添加了新的用戶,就為這個用戶分配一個線程,用於管理這個用戶將來想做的事情
pthread_t tid;
pthread_create(&tid,NULL,routine,(void *)new);
}
}
2、多路復用select()、FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()
(1).同時監聽多個IO口? ---- fd1 fd2 fd3 sockfd1 sockfd2 --> 集合中 --> 監聽集合就知道是誰有數據變化
阻塞IO? --> 監聽單個IO口,不能同時監聽多個。
非阻塞IO? --> 監聽多個IO口,但是占用CPU資源非常大。
--> 監聽多個IO口,又想不占用太多CPU資源 --> 多路復用。
(2).什么是多路復用? 工作原理?
首先用戶預先將需要進行監聽的所有的文件描述符加入集合中,然后在規定的時間/無限時間內無限等待集合。如果在規定的時間集合中文件描述符沒有數據變化,就會進入下一次規定時間內的等待。一旦集合中的文件描述符有數據變化,則其他沒有數據變化的文件描述符會被剔除到集合之外,並再次進入下一次的等待狀態。
(3).多路復用函數接口 --- select() --- man 2 select
#include<sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 集合中的所有文件描述符最大值+1
readfds: 所有關注"是否存在待讀取數據"的文件描述符集合 套接字sockfd 鍵盤:STDIN_FILENO 99%
writefds: 所有關注"是否有可傳輸非阻塞" 的文件描述符集合 -->0.5% NULL
exceptfds: 所有關注"是否發生異常"的文件描述符集合 -->0.5% NULL
timeout: 設置最大等待時間
---> 超時一次,重新設置該值,再傳遞給select函數
---> 如果該參數填NULL,則是無限等待
struct timeval {
long tv_sec; 秒
long tv_usec; 微秒 1秒 = 1000000微秒 ---> select函數可以精確到小數點后6位
};
返回值:
成功: 有數據達到 --> 就緒的文件描述符的總數
在規定的時間沒有數據到達 --> 0
失敗: -1
(4)、處理集合的函數
1)刪除集合set中某個文件描述符fd
void FD_CLR(int fd, fd_set *set);
2)判斷某個文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set); --> this is useful after select() returns.
返回值:
fd在集合中: 1
fd不在集合中: 0
3)把文件描述符fd加入到集合set中
void FD_SET(int fd, fd_set *set);
4)清空集合set
void FD_ZERO(fd_set *set);
例題:
實現客戶端與服務器進行收發,5秒內等待數據的到達! 如果5秒內沒有數據到達,則打印timeout!
客戶端 服務器
收 fd connfd --> 可以知道客戶端有沒有數據發送過來
發 STDIN_FILENO STDIN_FILENO --> 監聽自己的鍵盤有沒有數據的輸入
服務器/客戶端模型:
1. 處理TCP流程
2. 得到connfd/fd
3. 把connfd/fd與STDIN_FILENO加入讀集合readfdset中
4. 使用select函數監聽該集合
5. 判斷文件描述符是否在集合中
if(FD_ISSET(connfd/fd,&set) == 1)
{
read(connfd/fd,buf);
}
if(FD_ISSET(STDIN_FILENO,&set) == 1)
{
fgets(buf,50,stdin);
}
jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
server.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
void *routine(void*arg)
{
int i=0;
while(1)
{
printf("%d\n",i++);
sleep(1);
}
}
int main(int argc,char *argv[])
{
//0. 創建線程,用於計算時間流逝
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
//1. 創建套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//6. 把需要監聽的connfd加入集合中
fd_set rset;
struct timeval v;
char buf[50];
while(1)
{
//不管有沒有超時,每次都把套接字加入集合中
FD_ZERO(&rset);
FD_SET(connfd,&rset);
//重新設置超時時間
v.tv_sec = 5;
v.tv_usec = 0;
ret = select(connfd+1,&rset,NULL,NULL,&v);
//在5秒鍾內沒有數據達到,就打印timeout
if(ret == 0)
{
printf("timeout!\n");
}
//select函數執行失敗
if(ret == -1)
{
printf("select error!\n");
}
//在5秒內有數據達到,就打印數據
if(FD_ISSET(connfd,&rset) == 1)
{
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("buf:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
}
}
3.信號驅動signal()、fcntl()、
(1). 信號驅動工作原理是什么?
就是使用信號機制,首先安裝信號SIGIO處理函數,通過監聽文件描述符是否產生了SIGIO信號,當數據到達時,就等於產生該信號,用戶讀取該數據。
(2). 特點
1)信號驅動一般作用於UDP協議,很少作用於TCP協議,因為TCP協議中有多次IO口變化,難以捕捉信號。
2)由於有數據變化時,會產生一個信號,所以我們提前捕捉 -- signal(捕捉的信號,處理函數);
3)必須要給套接字/文件描述符設置添加一個信號觸發模式
(3). 在一個套接字上使用信號驅動,下面的三步是必須設置:
1)捕捉信號,設置信號的處理函數
signal(SIGIO,fun); --> fun()進行IO操作
2)設置套接字的擁有者(系統中有可能有很多套接字,必須提前告知是本進程的套接字)
F_SETOWN (long)
Set the process ID or process group ID that will receive SIGIO
and SIGURG signals for events on file descriptor fd to the ID
given in arg. A process ID is specified as a positive value;
fcntl(fd,F_SETOWN,getpid());
3)給套接字添加信號觸發模式
int state;
state = fcntl(fd,F_GETFL);
state |= O_ASYNC;
fcntl(fd,F_SETFL,state);
例題:
使用IO模型中信號驅動方式寫一個UDP服務器,實現不斷讀取客戶端消息。
思路:
1. 建立UDP套接字
2. 捕捉,設置擁有者,添加信號觸發模式
3. 一旦有數據到達,那么就在信號處理函數中不斷打印客戶端消息
jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 192.168.0.243 50002
{
//1. 創建UDP套接字(沒有地址的信箱)
int fd = socket(AF_INET,SOCK_DGRAM,0);
//2. 准備服務器的地址
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//AF_INET: 協議,與socket第一個參數一致
//argv[1]: 代表一個字符串,"192.168.0.243"
//&srvaddr.sin_addr: 代表struct in_addr *類型,使用srvaddr變量訪問sin_addr這個變量,再取地址就變成指針了!
/*
struct sockaddr_in
{
u_short sin_family; // 地址族
u_short sin_port; // 端口
struct in_addr sin_addr; // IPV4地址
char sin_zero[8];
};
*/
//3. 不斷寫信
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
if(strncmp(buf,"quit",4) == 0)
break;
}
//4. 回收套接字資源
close(fd);
return 0;
}
server.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int sockfd;
void fun(int sig)
{
printf("catch sig:%d\n",sig);
char buf[50];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
bzero(buf,50);
bzero(&cliaddr,len);
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);
printf("from client:%s",buf);
return;
}
int main(int argc,char *argv[]) // ./server 50001
{
//1. 創建UDP套接字
sockfd = socket(AF_INET,SOCK_DGRAM,0);
//2. 綁定IP地址到套接字上
struct sockaddr_in srvaddr;
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(srvaddr));
//3. 捕捉SIGIO信號,設置信號處理函數
signal(SIGIO,fun);
//4. 設置套接字的擁有者
fcntl(sockfd,F_SETOWN,getpid());
//5. 給套接字添加信號觸發模式
int state;
state = fcntl(sockfd,F_GETFL);
state |= O_ASYNC;
fcntl(sockfd,F_SETFL,state);
//6. 掛起進程,不退出
while(1)
pause();
return 0;
}
(八).設置屬性函數setsockopt()
setsockopt設置屬性函數
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
sockfd:需要設置屬性的套接字
level:優先級
SOL_SOCKET:套接字
IPPROTO_IP:IP優先級
IPPRO_TCP:TCP優先級
optname:選項名字
optval:值,使能為1,不使能為0 int struct timeval
optlen:值類型大小 sizeof(int) sizeof(struct timeval)
optname:
===========================SOL_SOCKET====================================:
optname選項名字 optlen的大小
SO_BROADCAST 允許發送廣播數據 int
SO_DEBUG 允許調試 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 獲得套接字錯誤 int
SO_KEEPALIVE 保持連接 int
SO_LINGER 延遲關閉連接 struct linger
SO_OOBINLINE 帶外數據放入正常數據流 int
SO_RCVBUF 接收緩沖區大小 int
SO_SNDBUF 發送緩沖區大小 int
SO_RCVLOWAT 接收緩沖區下限 int
SO_SNDLOWAT 發送緩沖區下限 int
SO_RCVTIMEO 接收超時 struct timeval
SO_SNDTIMEO 發送超時 struct timeval
SO_REUSEADDR 允許重用本地地址和端口 int
SO_TYPE 獲得套接字類型 int
SO_BSDCOMPAT 與BSD系統兼容 int
=========================IPPROTO_IP=======================================
IP_HDRINCL 在數據包中包含IP首部 int
IP_OPTINOS IP首部選項 int
IP_TOS 服務類型
IP_TTL 生存時間 int
IP_ADD_MEMBERSHIP 加入組播 struct ip_mreq
=========================IPPRO_TCP======================================
TCP_MAXSEG TCP最大數據段的大小 int
TCP_NODELAY 不使用Nagle算法 int
(九).網絡超時接收select、alarm、setsockopt
一般地,默認是阻塞等待讀取數據。有些場合不需要使用一直阻塞。因為一直阻塞可能沒有結果。這時候可以使用超時接收,在規定的時間內接收數據,超過規定的時間,就不會再阻塞。
設置超時接收數據方式:
1. 使用多路復用select函數設置超時時間。
2. 設置鬧鍾,當時間到達時,就會產生一個信號進行提醒,即超時。
3. 設置套接字本身的屬性為超時接收。
1、使用多路復用select函數設置超時時間。
例題:寫一個服務器進行接收數據,使用select函數監聽客戶端狀態,如果在5秒內沒有數據到達,則超時。
select只需要監聽 --> connfd --> 如果select返回值為0,則超時。
核心代碼:
while(1)
{
//不管有沒有超時,每次都把套接字加入集合中
FD_ZERO(&rset);
FD_SET(connfd,&rset);
//重新設置超時時間
v.tv_sec = 5;
v.tv_usec = 0;
ret = select(connfd+1,&rset,NULL,NULL,&v);
//只需要判斷套接字是否在集合中即可!
}
jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
server.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
void *routine(void*arg)
{
int i=0;
while(1)
{
printf("%d\n",i++);
sleep(1);
}
}
int main(int argc,char *argv[])
{
//0. 創建線程,用於計算時間流逝
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
//1. 創建套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//6. 把需要監聽的connfd加入集合中
fd_set rset;
struct timeval v;
char buf[50];
while(1)
{
//不管有沒有超時,每次都把套接字加入集合中
FD_ZERO(&rset);
FD_SET(connfd,&rset);
//重新設置超時時間
v.tv_sec = 5;
v.tv_usec = 0;
ret = select(connfd+1,&rset,NULL,NULL,&v);
//在5秒鍾內沒有數據達到,就打印timeout
if(ret == 0)
{
printf("timeout!\n");
}
//select函數執行失敗
if(ret == -1)
{
printf("select error!\n");
}
//在5秒內有數據達到,就打印數據
if(FD_ISSET(connfd,&rset) == 1)
{
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("buf:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
}
}
2、設置鬧鍾,當時間到達時,就會產生一個信號進行提醒,即超時。
鬧鍾這種方式類似信號驅動,信號驅動收到信號時,證明有數據過來。鬧鍾使用alarm函數來提前設置一個時間,當時間到達時,就會自動產生一個信號,證明超時。
例子: 設置一個鬧鍾,時間為5秒 --> 當時間到達時,就會自動產生一個SIGALRM信號。 14) SIGALRM
如何設定一個鬧鍾 --- alarm --- man 2 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds: 鬧鍾設置的時間 unsigned int --> 參數不能填負數!
//在seconds這么多秒之后就會產生一個SIGALRM信號給正在運行的進程
alarm() arranges for a SIGALRM signal to be delivered to the calling process in seconds seconds.
//如果秒數為0,不會預設定鬧鍾
If seconds is zero, no new alarm() is scheduled.
//任何的事件都可以使用alarm()取消 --> 重新設定時間,鬧鍾到點就不會響應。
In any event any previously set alarm() is canceled.
alarm(5); --> 如果順利倒數5秒,則會產生一個信號 SIGALRM
如果被重新設定時間,重新倒數!
返回值: 返回剩余的時間,如果倒數完了,返回0
例子:
while(1)
{
alarm(5); --> 倒數完,會產生一個信號SIGALRM
....;
....; --> 如果在這個地方阻塞了,就不會再去執行alarm(5),沒有重新預設定時間。
....;
}
例題: 設置一個鬧鍾,讓客戶端必須在5秒之內發送數據給服務器,如果服務器在5秒內收到數據,則重新倒數5秒。
如果在5秒內沒有收到數據,打印timeout。直到收到數據為止再去重新設置鬧鍾來倒數5秒。
jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 服務器IP 端口號 ./Jack 192.168.0.2 50001
{
//1. 創建未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0); // 必須與服務器的類型一致
//2. 准備對方Rose的IP地址,端口號,協議
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//3. 發起連接
socklen_t len = sizeof(srvaddr);
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//連接成功后,fd自身就會變成已連接套接字
if(ret == -1)
printf("connect error!\n");
else
printf("connect ok!\n");
//4. 暢聊
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
send(fd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 掛斷
close(fd);
return 0;
}
rose.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <signal.h>
#include <pthread.h>
void *routine(void *arg)
{
int i = 0;
while(1)
{
printf("%d\n",i++);
sleep(1);
}
}
void fun(int sig)
{
printf("catch sig = %d\n",sig);
printf("timeout!\n");
}
int main(int argc,char *argv[]) // ./Rose 50001
{
//0. 創建線程
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
signal(SIGALRM,fun);
//1. 創建一個未連接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
//2. 准備好服務器的結構體變量,再進行賦值
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET; //網際協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
//3. 把服務器的IP地址,協議,端口號綁定到未連接套接字上
socklen_t len = sizeof(srvaddr);
int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
if(ret == -1)
printf("bind error!\n");
//4. 將未連接套接字轉換為監聽套接字
listen(fd,4);
//5. 坐等電話
struct sockaddr_in cliaddr; //存放來電顯示
int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待
if(connfd == -1)
printf("accept error!\n");
else
printf("connect ok!\n");
//6. 暢聊
char buf[50];
while(1)
{
alarm(5); //如果5秒到了,就會產生一個信號SIGALRM,但是阻塞在recv
bzero(buf,50);
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
//7. 掛斷電話
close(connfd);
close(fd);
return 0;
}
4、設置套接字本身的屬性為超時接收。
在Linux中,默認創建的套接字都是阻塞屬性,我們需要設置一個超時屬性給套接字,這樣讀取套接字中數據時,在規定的時間之內會阻塞,在規定的時間之外,讀取失敗。
1. 例子:
int connfd = accept(fd);
read(connfd); --> 讀取會一直阻塞
int connfd = accept(fd);
設置一個超時的時間給connfd
read(connfd); --> 有數據 --> 讀取出來
--> 在規定的時間沒有數據 --> read函數就會馬上返回失敗,不會一直等待!
-
如何設置屬性給套接字? --- setsockopt --- man 2 setsockopt
#include <sys/types.h>/*See NOTES*/ #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); sockfd:需要設置屬性的套接字 level:優先級 SOL_SOCKET:套接字 IPPROTO_IP:IP優先級 IPPRO_TCP:TCP優先級 optname:選項名字 optval:值,使能為1,不使能為0 int struct timeval optlen:值類型大小 sizeof(int) sizeof(struct timeval) //返回值: 成功: 0 失敗: -1
例子: 添加一個接受超時屬性給套接字connfd
struct timeval v; v.tv_sec = 5; v.tv_usec = 0; setsockopt(connfd,SOL_SOCKET,SO_RCVTIMEO,&v,sizeof(v));
例題:使用套接字設置屬性函數設置超時屬性,如果服務器在6秒內沒有數據到達,則打印timeout!
(十).廣播、組播setsockopt
1. 廣播
之前介紹所有例子: "點對點" --> 在socket稱之為單播
如果給局域網中所有的主機發送數據: "點對多" --> 廣播
(1). 廣播特點:
1)不是循環地給每個點發送數據,而是在一個局域網中,給廣播的地址(xxx.xxx.xxx.255)發送消息
2)只需要給廣播地址發送消息,整個網段的主機都會收到消息
192.168.1.100 192.168.1.243 192.168.1.255
“hello”
3)只有UDP協議才能使用廣播
(2). 廣播地址:
gec@ubuntu:/mnt/hgfs/fx9/02 網絡編程/03/code/timeout/setsockopt$ ifconfig
eth0 Link encap:Ethernet HWaddr 00:0c:29:f5:92:f6
inet addr:192.168.0.243 ---> 當前主機的IP地址
Bcast:192.168.0.255 ---> 廣播地址
Mask:255.255.255.0 ---> 子網掩碼
如果給192.168.0.255發送數據,那么整個“192.168.0.xx”網段主機都會收到消息
如果給255.255.255.255發送數據,無論你是什么網段的主機,都會收到消息
(3). 如何使得客戶端發送廣播數據?
在Linux中創建套接字默認是沒有廣播的屬性,所以手動添加廣播屬性給套接字
1)建立UDP套接字
int sockfd = socket(UDP協議); --> sockfd是沒有廣播屬性
2)設置廣播的屬性給套接字
setsockopt(sockfd,廣播屬性);
3)往廣播的地址上發送數據
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); // ./Jack 192.168.0.255 50001
例題:寫一個客戶端,實現廣播地發送消息
Ubuntu: 192.168.0.243 ./server 50001
同桌: 192.168.0.244 ./server 50001
寫一個廣播客戶端client:
執行: ./client 192.168.0.243 50001 單播
./client 192.168.0.255 50001 廣播
./client 255.255.255.255 50001 廣播
2. 組播
組播算是單播與廣播之間的折中,在一個局域網中,把某些主機加入組,設置一個IP地址給組。將來我們只需要往組的地址上發送數據,那么加入該組的所有主機都會收到數據。
(1). 特點:
1)在組播之前必須為組設置一個D類地址作為該組的一個IP地址 224.0.0.10
2)只有UDP協議才能實現組播
(2). IP地址分類: 192.168.0.100(網絡字節+主機字節)
網絡字節 主機字節 范圍
A類地址: 1字節 3字節 1.0.0.1 ~ 126.255.255.255
B類地址: 2字節 2字節 128.0.0.1 ~ 191.255.255.255
C類地址: 3字節 1字節 192.0.0.1 ~ 223.255.255.255
D類地址: 不區分網絡字節與主機字節 224.0.0.1 ~ 239.255.255.255
(3). 服務器怎么接受組播消息? --> 需要添加加入組播屬性到套接字上
加入組播屬性: IP_ADD_MEMBERSHIP 加入組播 struct ip_mreq
該結構體是被定義在Ubuntu: /usr/include/linux/in.h
struct ip_mreq {
struct in_addr imr_multiaddr; //組播的組的IP地址 224.0.0.10
struct in_addr imr_interface; //需要加入到組里面IP地址 192.168.0.243 -> 就是這個IP地址進組
};
struct in_addr
{
in_addr_t s_addr; // 無符號32位網絡地址
};
服務器框架:
1)建立UDP套接字
int sockfd = socket(UDP協議);
2)定義struct ip_mreq變量
struct ip_mreq v;
inet_pton(AF_INET,"224.0.0.10",&v.imr_multiaddr);
inet_pton(AF_INET,"192.168.0.243",&v.imr_interface);
3)加入組播屬性到套接字上
setsockopt(sockfd,.........,&v,sizeof(v));
4)坐等組播消息
客戶端框架:
1)建立UDP套接字
int sockfd = socket(UDP協議);
2)設置廣播的屬性給套接字
setsockopt(sockfd,廣播屬性);
3)發送數據給服務器
./Jack 192.168.0.243 50001 單播
./Jack 224.0.0.10 50001 組播
./Jack 192.168.0.255 50001 廣播
./Jack 255.255.255.255 50001 廣播
例子: 服務器1 --> 224.0.0.10
服務器2 --> 224.0.0.10
服務器3不加入組
./Jack 224.0.0.10 50001 --> 只有服務器1與服務器2才能收到數據
jack.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Jack 192.168.0.255 50002
{
//1. 創建UDP套接字(沒有地址的信箱)
int fd = socket(AF_INET,SOCK_DGRAM,0);
//1.5 設置套接字的廣播屬性
int on = 1;
setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
//2. 准備服務器的地址
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
//AF_INET: 協議,與socket第一個參數一致
//argv[1]: 代表一個字符串,"192.168.0.243"
//&srvaddr.sin_addr: 代表struct in_addr *類型,使用srvaddr變量訪問sin_addr這個變量,再取地址就變成指針了!
/*
struct sockaddr_in
{
u_short sin_family; // 地址族
u_short sin_port; // 端口
struct in_addr sin_addr; // IPV4地址
char sin_zero[8];
};
*/
//3. 不斷寫信
char buf[50];
while(1)
{
bzero(buf,50);
fgets(buf,50,stdin);
sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
if(strncmp(buf,"quit",4) == 0)
break;
}
//4. 回收套接字資源
close(fd);
return 0;
}
rose.c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h> //18.04 16.04 刪除這個頭文件
#include <strings.h>
#include <string.h>
int main(int argc,char *argv[]) // ./Rose 50001
{
//1. 創建UDP套接字(沒有地址的信箱)
int fd = socket(AF_INET,SOCK_DGRAM,0);
//2. 准備服務器的IP地址(准備地址)
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //協議
srvaddr.sin_port = htons(atoi(argv[1])); //端口號
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址
//3. 綁定地址到套接字(把准備好的地址綁定到信箱上)
bind(fd,(struct sockaddr *)&srvaddr,len);
//4. 不斷從UDP套接字中接收數據
struct sockaddr_in cliaddr;
char buf[50];
while(1)
{
bzero(buf,50);
//不斷從fd這個信箱上讀取cliaddr這個客戶端給我發來的內容,然后存放在buf中
recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);
printf("from %s : %s",(char *)inet_ntoa(cliaddr.sin_addr),buf);
if(strncmp(buf,"quit",4) == 0)
break;
}
//5. 關閉套接字資源
close(fd);
return 0;
}