TCP通信服務器端實現第一步:調用socket 網絡API,創建套接字文件(TCP/IP網絡編程)【linux】(zzt)

TCP通信的代碼舉例

· 寫一個TCP服務器程序
· 寫一個客戶端程序
然後客戶端向服務器請求連接,當連接成功之後,服務器和客戶端程序實現通信。

不過爲了不要讓例子太複雜,服務器程序目前只爲一個客戶服務,至於使用多進程、多線程以及多路io爲多客戶服務的情況,後面進行說明。

實現TCP服務器程序

按照TCP編程模型來編寫

第1步:調用socket 網絡API,創建套接字文件

socket函數

函數原型

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

功能

創建一個套接字文件,然後以文件形式來操作通信,不過套接字文件沒有文件名。

Linux有7種文件,套接字文件就是其中一種。

socket翻譯爲中文就是“套接字”的意思,其實翻譯爲插座更合適些,因爲socket本來就有插座的意思,使用socket創建一個通信的套接字文件,就好比給應用程序插上了一個通信的插座,有了這個插座就可以和對方應用程序通信了。

圖解說明:

socket函數說明

返回值

  • 成功:返回套接字文件描述符。
  • 失敗:返回-1,errno被設置。

參數

domian 指定協議族

族/範圍

int socket(int domain, int type, int protocol);
  • 作用:指定協議族

爲什麼要指定協議族?
因爲你要使用的通信協議一定是屬於某個協議族,所以如果不指定協議族,又怎麼指定協議族中的某個具體協議呢。
比如我們想用的是TCP協議,TCP屬於TCP/IP協議族中的子協議,所以必須先通過domain指定TCP/IP協議族,不過TCP/IP協議族有兩個版本,分別是IPV4是IPV6版本,我們目前使用的還是IPV4版本,因爲Ipv6還未普及。

IPV4是Internet Protocol Version4的縮寫,直譯爲“網絡協議第四版”,IPV4和IPV6這兩個版本所使用的ip地址格式完全不同,
IPV4:ip爲32位
IPV6:ip爲128位

不僅IPV4和IPV6的ip地址格式不同,其實所有不同網絡協議族所使用ip地址格式都不相同。

int socket(int domain, int type, int protocol);
  • domain可設置的常見宏值。

可設置的有:AF_UNIX, AF_LOCAL(前面兩個宏明表示的意義是一樣的)、AF_INET、AF_INET6AF_IPXAF_NETLINKAF_APPLETALKAF_PACKET、AF_UNSPEC,有關每個宏的作用後面再解釋,這裏先介紹下這些“宏名”。

AF是address family,表示地址家族的意思,由於每個網絡協議族的ip地址格式完全不同,因此在指定ip地址格式時需要做區分,所以這些AF_***宏就是用於說明所使用的是什麼協議族的IP地址,也就是說定義這些宏的目的是用來區分不同協議族的IP地址格式。

這些個宏定義在了socket.h中
我們下面給出一部分宏名對應的內容。

#define AF_UNSPEC	0
#define AF_UNIX		1	/* Unix domain sockets 		*/
#define AF_LOCAL	1	/* POSIX name for AF_UNIX	*/
#define AF_INET		2	/* Internet IP Protocol 	*/
#define AF_AX25		3	/* Amateur Radio AX.25 		*/
...

有讀者可能會說不對呀
domain不是用來指定協議族的嗎,但是AF_ * * * 確是用來區分不同協議ip格式的,給domain指定AF_ * * * 合適嗎?

其實是合適的,沒有問題。實際上區分不同協議族應該使用PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC,PF就是protocol family的意思,意思是“協議家族”。

PF_***定義的宏與AF_***定義的宏不同的只是前綴,不過AF_***宏與PF_***宏對應的值完全一樣,比如AF_UNIX == PF_UNIX,所以給domain指定AF_***,與指定PF_***完全是一樣的。

爲什麼AF_ * * * 定義的宏 == PF_ * * * 定義的宏?

AF_***用於區分不同協議族的ip地址格式,而PF_***則用於區分不同的協議族,但是每個協議族的IP格式就一種,所以協議族與自己的IP格式其實是一對一的,因此如果你知道使用的是什麼ip地址格式,其實你也就知道了使用的是什麼協議族,所以使用AF_***定義的宏也可以用於區分不同的協議族。

不過爲了更加正規一點,區分不同協議族的宏還是被命名爲了PF_***,只不過它的值就是AF_***的值。

#define PF_UNSPEC	AF_UNSPEC
#define PF_UNIX		AF_UNIX
#define PF_LOCAL	AF_LOCAL
#define PF_INET		AF_INET
#define PF_AX25		AF_AX25
...
int socket(int domain, int type, int protocol);

總之希望讀者理解,domain是用於指定協議族的,設置的宏可以是AF_***,也可以是PF_***,不過正規一點的話還是應該寫PF_***,因爲這個宏纔是專門用來區分協議族的,而AF_***則是用來區分不同協議族的ip地址格式的。

不過socket的手冊裏面寫的都是AF_***,沒有寫PF_***。

socket手冊查看

  • domain的常見宏值,各自代表是什麼協議族,制定不同的宏就代表不同的協議族。

AF_UNIX, AF_LOCAL、AF_INET、AF_INET6、AF_IPX、AF_NETLINK、AF_APPLETALK、AF_PACKET、AF_UNSPEC。
上面宏值與下面的宏值對應相等。
PF_UNIX, PF_LOCAL、PF_INET、PF_INET6、PF_IPX、PF_NETLINK、PF_APPLETALK、PF_PACKET、PF_UNSPEC。

  • PF_UNIX、PF_LOCAL:域通信協議族
    這兩個宏值是一樣(宏值都是1)。

給domain指定該宏時就表示,要使用的是“本機進程間通信”協議族,域通信也就是本機進程間通信,我們在說明IPC時就說過,還有一種域套接字的IPC,也可以專門用來實現“本機進程間通信”,域套接字IPC就是我們這裏說明的域通信。

這裏域就是本機的意思,當我們給socket的domain指定這兩個宏時,創建的就是域套接字文件,這個套接字文件是用來實現域通信的,所以把創建的文件就稱爲域套接字文件。
後面說明域套接字通信時,會具體的來使用這個宏。

  • PF_INET:指定ipv4的TCP/IP協議族。
    我們這篇博客要說明的TCP和UDP就是IPV4的TCP和UDP。

  • PF_INET6:ipv6的TCP/IP協議族,目前還未普及使用,我們這裏不說明。

  • PF_IPX:novell協議族,幾乎用不到,瞭解即可。

novell協議族由美國Novell網絡公司開發的一種專門針對局域網的“局域網通信協議”。

這個協議的特點是效率較高,所以好多局域網遊戲很喜歡使用這個協議來進行局域網通信,比如以前的局域網遊戲CS,據說使用的就是novell協議族。

之所以使用novell協議族,是因爲CS的畫面數據量太大,而且協同性要求很高,所以就選擇了使用novell協議族這個高效率的局域網協議。

現在互聯網使用的都是TCP/IP協議,而novell和TCP/IP是兩個完全不同的協議,所以使用novell協議族的局域網與使用TCP/IP協議族的互聯網之間兼容性比較差,如果novell協議的局域網要接入TCP/IP的Internet的話,數據必須進行協議轉換。

所謂協議轉換就是
novell局域網的數據包發向TCP/IP的互聯網時,將novell協議族的數據包拆包,然後重新封包爲TCP/IP協議的數據包。

TCP/IP的互聯網數據包發向novell局域網時,將TCP/IP協議族的數據包拆包,然後重新封包爲novell協議的數據包。

windows似乎並不是支持novell協議,但是Linux、unix這邊是支持的。

我怎麼知道是支持的?
既然我們在socket手冊中查到了PF_IPX,就說明時肯定支持。
socket手冊查看

  • PF_APPLETALK:蘋果公司專爲自己“蘋果電腦”設計的局域網協議族。

  • AF_UNSPEC:不指定具體協議族
    int socket(int domain, int type, int protocol);

此時可以通過第三個參數protocol指定協議編號來告訴socket,你使用的是什麼協議族中的哪個子協議。

什麼是協議編號?
每個協議的唯一識別號,制訂協議的人給的。

type 套接字類型

套接字類型,說白了就是進一步指定,你想要使用協議族中的那個子協議來通信。

比如,如果你想使用TCP協議來通信,
首先:將domain指定爲PF_INET,表示使用的是IPV4的TCP/IP協議族
其次:對type進行相應的設置,進一步表示我想使用的是TCP/IP協議族中的TCP協議。

type的常見設置值:SOCK_STREAM、SOCK_DGRAM、SOCK_RDM、SOCK_NONBLOCK、SOCK_CLOEXEC。

  • SOCK_STREAM:
int socket(int domain, int type, int protocol);	

將type指定爲SOCK_STREAM時,表示想使用的是“有序的、面向連接的、雙向通信的、可靠的字節流通信”,並且支持帶外數據。

有關什麼是帶外數據我們後面進行說明。
stream 就是字節流的意思。

int socket(int domain, int type, int protocol);	

如果domain被設置爲PF_INET,type被設置爲SOCK_STREAM,就表示你想使用TCP來通信,因爲在TCP/IP協議族裏面,只有TCP協議是“有序的、面向連接的、雙向的、可靠的字節流通信”。

使用TCP協議通信時TCP並不是孤立的,它還需要網絡層和鏈路層協議的支持才能工作。

如果type設置爲SOCK_STREAM,但是domain指定爲了其它協議族,那就表示使用的是其它“協議族”中類似TCP這樣的可靠傳輸協議。

  • SOCK_DGRAM:
int socket(int domain, int type, int protocol);	

將type指定爲SOCK_DGRAM時,表示想使用的是“無連接、不可靠的、固定長度的數據報通信”。

固定長度意思是說,分組數據的大小是固定的,不管網絡好不好,不會自動去調整分組數據的大小,所以“固定長度數據報”其實就是“固定長度分組數據”的意思。

當domain指定爲PF_INET、type指定爲SOCK_DGRAM時,就表示想使用的是TCP/IP協議族中的中的UDP協議,因爲在TCP/IP協議族中,只有UDP是“無連接、不可靠的、固定長度的數據報通信”。

同樣的UDP協議通信不可能獨立工作,需要網絡層和鏈路層協議的支持。

如果type設置爲SOCK_DGRAM,但是domain指定爲了其它協議族,那就表示使用的是其它“協議族”中類似UDP這樣的不可靠傳輸協議。
後面說明UDP時就會使用SOCK_DGRAM。

  • SOCK_RDM:表示想使用的是“原始網絡通信”。
int socket(int domain, int type, int protocol);	

當domain指定爲TCP/IP協議族、type指定爲SOCK_RDM時,就表示想使用ip協議來通信,直接使用IP協議來通信,其實就是原始的網絡通信。

我們在前面說明TCP/IP協議族的時候,應用程序可以直接越過TCP協議或者UDP協議,直接使用IP協議進行通信,也就是原始的網絡通信。

TCP/IP協議族

爲什麼稱爲原始通信?
以TCP/IP協議族爲例,TCP/IP之所以能夠實現網絡通信,最根本的原因是因爲IP協議的存在,ip協議纔是關鍵,只是原始的IP協議只能實現最基本的通信,還缺乏很多精細的功能,所以纔多了基於IP工作的TCP協議和UDP協議這兩種傳輸層協議,TCP/UDP彌補了IP缺失的精細功能。

儘管ip提供的只是非常原始的通信,但是我們確實可以使用比較原始的IP協議來進通信,特別是當你不滿意TCP和UDP的表現,你想實現一個比TCP和UDP更好的傳輸層協議時,你就可以直接使用ip協議,然後由自己的應用程序來實現符合自己要求的類似tcp/udp協議,不過我們幾乎遇不到這種情況,這裏瞭解下即可。

如果type設置爲SOCK_RDM,但是domain指定爲了其它協議族,那就表示使用的是其它“協議族”中類似ip這樣最基本的原始通信協議。

  • SOCK_NONBLOCK:將socket返回的文件描述符指定爲非阻塞的。
int socket(int domain, int type, int protocol);

如果不指定這個宏的話,使用socket返回“套接字文件描述符”時,不管是是用來“監聽”還是用來通信,都是阻塞操作的,但是指定SOCK_NONBLOCK這個宏的話,就是非阻塞的。

當然也可以使用fcntl函數來指定SOCK_NONBLOCK,至於fcntl怎麼用,我們在高級IO博客中已經有非常詳細的說明。

SOCK_NONBLOCK宏可以和前面的宏進行 | 運算,比如:
SOCK_STREAM | SOCK_NONBLOCK
上面的宏表示使用面向連接的、可靠的字節流通信,同時socket返回的文件描述符非阻塞。

  • SOCK_CLOEXEC:表示一旦進程exec執行新程序後,自動關閉socket返回的“套接字文件描述符”。
int socket(int domain, int type, int protocol);		

圖解說明:

SOCK_CLOEXEC宏說明

再次說明,type指定 SOCK_CLOEXEC 宏,一旦進程通過調用exec函數去執行新程序,之前調用socket函數返回的套接字文件描述符將會自動關閉,在新程序裏面無法使用上面的套接字文件描述符sockfd。

這個標誌也是可以和前面的宏進行 | 運算的,不過一般不指定這個標誌。
例如:SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC

int socket(int domain, int type, int protocol);		
protocol 指定協議號

一般情況下protocol寫0,表示使用domain和type所指定的協議。

不過如果domain和type所對應的協議有好幾個時,此時就需要通過具體的協議號來區分了,否則寫0即可,表示domain和type所對應的協議就一個,不需要指定協議號來區分。

一般情況下使用TCP的時候,domain和type 兩個參數所指定的TCP協議就只有一個,
所以第三個參數設置爲0即可。

在哪裏可以查到協議號?
所有協議的協議號都被保存在了/etc/protocols下。
打開文件查看協議號:vi /etc/protocols
查看協議號

協議 編號
ip 對應 0
icmp 對應 1
igmp 對應 2
tcp 對應 6
udp 對應 17

如果現在要調用socket 函數使用tcp協議,那麼通過下面方式指定參數:

socket(PF_INET, SOCK_STREAM, 0);	

我們可以看到上面指定的TCP協議標號爲6。
如果調用socket 函數爲:

socket(PF_INET, SOCK_STREAM, 6);	

也不會出現錯誤,相當於重複指定。通過前兩個參數本來就可以指定TCP協議,結果又指定了TCP協議號。不過一般來說如果前面兩個參數能夠確定協議,那麼就不指定協議編號,協議編號寫0即可。因爲指定協議號會更加麻煩,而且協議號也記不住。除非前面兩個參數所對應的協議分成了好幾個子協議,那麼就必須通過協議編號進行區分。

代碼演示

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>

void  print_err(char * str,int line,int err_no)//出錯處理函數
{
	printf("%d,%s: %s\n",line,str,strerror(err_no));
	exit(-1);
}
int main(void)
{
	int sockfd = -1; //存放套接字文件描述符
	/*創建使用TCP協議通信的套接字文件*/
	sockfd =  socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
	if(-1 == sockfd) //進行出錯處理
		print_err("socket fail",__LINE__,errno);
	return 0;
}

運行結果爲:
調用socket函數運行結果

代碼運行沒有問題。下篇博客進行第二步:調用bind函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章