【Linux】 Linux網路程式設計

李春港發表於2021-05-18

作者:李春港
出處:https://www.cnblogs.com/lcgbk/p/14779410.html

前言

本文章主要是講解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 伺服器

  1. 建立未連線TCP套接字

    int fd = socket(AF_INET,SOCK_STREAM,0);
    
  2. 準備好伺服器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)
    
  3. 把地址繫結到未連線套接字上

    socklen_t就是struct sockaddr_in大小的資料型別
    socklen_t len = sizeof(srvaddr);
    
    int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
    
  4. 設定監聽套接字

    listen(fd,4);   是fd的本身從未連線套接字轉換監聽套接字
    /*
    backlog引數就是控制我們的已連線佇列裡等待accept()取走的連線的最大數目的.注意一點,backlog與這個已排隊連線的最大數目未必是完全相等的,不同的系統的實現可能不同.比如backlog=1,系統允許的實際一排隊數目可能為2.
    */
    
  5. 坐等對方的連線

    int connfd = accept(fd,(strutc sockaddr*)&cliaddr,&len);
    
  6. 暢聊

    recv(connfd,buf,sizeof(buf),0);
    
  7. 斷開連線

    close(connfd);
    close(fd);
    

核心程式碼 Jack.c 客戶端

  1. 建立未連線TCP套接字

    int fd = socket(AF_INET,SOCK_STREAM,0);
    
  2. 發起連線

    int ret = connect(fd,(struct sockaddr *)&srvaddr,len);
    
  3. 暢聊

    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函式就會馬上返回失敗,不會一直等待!
  1. 如何設定屬性給套接字? --- 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;
}

相關文章