前言
使用TCP通信時,TCP協議要求必須要有一個服務器端。這一點是由TCP協議本身的特性決定的,只要你使用TCP協議來通信,就必須要有一個TCP服務器端。
TCP服務器的大概工作過程
(1)服務器會使用專門“文件描述符”來監聽客戶的“三次握手”,然后建立連接。
(2)一旦連接建立成功后,服務器會分配一個專門的 “通信文件描述符”,用於實現與該連接客戶的通信
由於建立連接時,雙方的TCP協議都已經記住了對方IP和端口,所以雙方正式通信時,TCP會自動使用記錄的IP和端口,我們不需要重新指定對方的IP和端口。
TCP編程模型
在編程模型里面,必須要有一方是TCP服務器,另一方是TCP客戶。服務器只有一個,但是客戶端有很多,不管客戶端有多少個,客戶端與服務器端的通信,都按照編程模型的描述來實現的。
API
socket
原型
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
功能
創建一個套接字文件,然后以文件形式來操作通信,不過套接字文件沒有文件名。
參數
domian
作用:指定協議族
為什么要指定協議族?
因為你要使用的通信協議一定是屬於某個協議族,所以如果不指定協議族,又怎么指定協議族中的某個具體協議呢。比如我們想用的是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地址格式都不相同。
domain可設置的常見宏值
可設置的有:AF_UNIX, AF_LOCAL、AF_INET、AF_INET6、AF_IPX、AF_NETLINK、AF_APPLETALK、AF_PACKET、AF_UNSPEC
AF是address family,表示地址家族的意思,由於每個網絡協議族的ip地址格式完全不同,因此在指定ip地址格式時需要做區分,所以這些AF_***宏就是用於說明所使用的是什么協議的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 ...
domain是用於指定協議族的,設置的宏可以是AF_***,也可以是PF_***,不過正規一點的話還是應該寫PF_***,因為這個宏才是專門用來區分協議族的,而AF_***則是用來區分不同協議族的ip地址格式的。不過socket的man手冊里面寫的都是AF_***,沒有寫PF_***。
domain的常見宏值,各自代表是什么協議族
PF_UNIX、PF_LOCAL:域通信協議族
這兩個宏值是一樣(宏值都是1)。給domain指定該宏時就表示,你要使用的是“本機進程間通信”協議族。域套接字的IPC,也可以專門用來實現“本機進程間通信”。這個域就是本機的意思,當我們給socket的domain指定這個宏時,創建的就是域套接字文件。
PF_INET:指定ipv4的TCP/IP協議族。
PF_INET6:ipv6的TCP/IP協議族,目前還未普及使用
PF_IPX: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這邊是支持的。
PF_APPLETALK:蘋果公司專為自己“蘋果電腦”設計的局域網協議族。
AF_UNSPEC:不指定具體協議族
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:
將type指定為SOCK_STREAM時,表示想使用的是“有序的、面向連接的、雙向通信的、可靠的字節流通信”,並且支持帶外數據。
如果domain被設置為PF_INET,type被設置為SOCK_STREAM,就表示你想使用TCP來通信,因為在TCP/IP協議族里面,只有TCP協議是“有序的、面向連接的、雙向的、可靠的字節流通信”。
使用TCP通信時TCP並不是孤立的,它還需要網絡層和鏈路層協議的支持才能工作。
如果type設置為SOCK_STREAM,但是domain指定為了其它協議族,那就表示使用的是其它“協議族”中類似TCP這樣的可靠傳輸協議。
SOCK_DGRAM:
將type指定為SOCK_DGRAM時,表示想使用的是“無連接、不可靠的、固定長度的數據報通信”。
固定長度意思是說,分組數據的大小是固定的,不管網絡好不好,不會自動去調整分組數據的大小,所以“固定長度數據報”其實就是“固定長度分組數據”的意思。
當domain指定為PF_INET、type指定為SOCK_DGRAM時,就表示想使用的是TCP/IP協議族中的中的DUP協議,因為在TCP/IP協議族中,只有UDP是“無連接、不可靠的、固定長度的數據報通信”。
同樣的UDP不可能獨立工作,需要網絡層和鏈路層協議的支持。
如果type設置為SOCK_DGRAM,但是domain指定為了其它協議族,那就表示使用的是其它“協議族”中類似UDP這樣的不可靠傳輸協議。
SOCK_RDM:
表示想使用的是“原始網絡通信”。
比如,當domain指定為TCP/IP協議族、type指定為SOCK_RDM時,就表示想使用ip協議來通信,使用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返回的文件描述符指定為非阻塞的。
如果不指定這個宏的話,使用socket返回“套接字文件描述符”時,不管是是用來“監聽”還是用來通信,都是阻塞操作的,但是指定這個宏的話,就是非阻塞的。當然也可以使用fcntl來指定SOCK_NONBLOCK,至於fcntl怎么用,參考高級IO。
SOCK_NONBLOCK宏可以和前面的宏進行 | 運算,比如:SOCK_STREAM | SOCK_NONBLOCK
SOCK_CLOEXEC:
表示一旦進程exec執行新程序后,自動關閉socket返回的“套接字文件描述符”。
這個標志也是可以和前面的宏進行 | 運算的,不過一般不指定這個標志。
SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC
protocol
指定協議號。一般情況下protocol寫0,表示使用domain和type所指定的協議。不過如果domain和type所對應的協議有好幾個時,此時就需要通過具體的協議號來區分了,否者寫0即可,表示domain和type所對應的協議就一個,不需要指定協議號來區分。
疑問:在哪里可以查到協議號?
所有協議的協議號都被保存在了/etc/protocols下。
協議 編號 ip 0 icmp 1 igmp 2 tcp 6 udp 17 等
返回值
成功:返回套接字文件描述符。 失敗:返回-1,errno被設置
值 | 含義 |
---|---|
EACCES | 沒有權限建立制定的domain的type的socket |
EAFNOSUPPORT | 不支持所給的地址類型 |
EINVAL | 不支持此協議或者協議不可用 |
EMFILE | 進程文件表溢出 |
ENFILE | 已經達到系統允許打開的文件數量,打開文件過多 |
ENOBUFS/ENOMEM | 內存不足。socket只有到資源足夠或者有進程釋放內存 |
EPROTONOSUPPORT | 制定的協議type在domain中不存在 |
bind
原型
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能
將指定了通信協議(TCP)的套接字文件與IP以及端口綁定起來。
注意,綁定的一定是自己的Ip和端口,不是對方的,比如對於TCP服務器來說,綁定的就是服務器自己的ip和端口。
參數
sockfd:套接字文件描述符,代表socket創建的套接字文件。
addrlen:第二個參數所指定的結構體變量的大小
addr:
struct sockaddr結構體變量的地址,結構體成員用於設置你要綁定的ip和端口。
結構體成員:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
sa_family:指定AF_***,表示使用的什么協議族的IP,前面說過,協議族不同,ip格式就不同
sa_data:存放ip和端口
如果將ip和端口直接寫入sa_data數組中,雖然可以做到,但是操作起來有點麻煩,不過好在,我們可以使用更容易操作的struct sockaddr_in結構體來設置。不過這個結構體在在bind函數的手冊中沒有描述。
struct sockaddr_in { sa_family_t sin_family; //設置AF_***(地址族) __be16 sin_port; //設置端口號 struct in_addr sin_addr; //設置Ip /* 設置IP和端口時,這個成員用不到,這個成員的作用后面再解釋, */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; };
struct in_addr {
__be32 s_addr; //__be32是32位的unsigned int,因為IPV4的ip是32位的無符號整形數
};
在struct sockaddr_in中,存放端口和ip的成員是分開的,所以設置起來很方便。使用struct sockaddr_in設置后,然后將其強制轉為struct sockaddr類型,然后傳遞給bind函數即可。
struct sockaddr_in的使用例子:
struct sockaddr_in addr; addr.sin_family = AF_INET; //使用是IPV4 TCP/IP協議族的ip地址(32位) addr.sin_port = htons(5006); /指定端口 addr.sin_addr.s_addr = inet_addr("192.168.1.105"); //指定ip地址 ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
注意,如果是跨網通信時,綁定的一定是所在路由器的公網ip。bind會將sockfd代表的套接字文件與addr中設置的ip和端口綁定起來。
返回值
成功返回0,失敗返回-1,errno被設置。
|值 | 含義 | 備注 | |----------- |------------------------------------|-----------------| |EADDRINUSE |給定地址已經使用,實際上是端口被使用 |EBADF |sockfd不合法 |EINVAL |sockfd已經綁定到其他地址 |ENOTSOCK |sockfd是一個文件描述符,不是socket描述符 |EACCES |地址被保護,用戶的權限不足 |EADDRNOTAVAIL|接口不存在或者綁定地址不是本地 |UNIX協議族,AF_UNIX |EFAULT |my_addr指針超出用戶空間 |UNIX協議族,AF_UNIX |EINVAL |地址長度錯誤,或者socket不是AF_UNIX族 |UNIX協議族,AF_UNIX |ELOOP |解析my_addr時符號鏈接過多 |UNIX協議族,AF_UNIX |ENAMETOOLONG |my_addr過長 |UNIX協議族,AF_UNIX |ENOENT |文件不存在 |UNIX協議族,AF_UNIX |ENOMEN |內存內核不足 |UNIX協議族,AF_UNIX |ENOTDIR |不是目錄 |UNIX協議族,AF_UNIX
到底什么是綁定?
所謂綁定就是讓套接字文件在通信時,使用固定的IP和端口。
對於TCP的服務器來說,必須綁定。
對於TCP通信的客戶端來說,自動指定ip和端口是常態。客戶與服務器建立連接時,服務器會從客戶的數據包中提取出客戶的ip和端口,並保存起來。
htons
原型
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
功能
功能有兩個
1. 將端口號從“主機端序”轉為“網絡端序”
2. 如果給的端口不是short,將其類型轉為short型
htons:是host to net short的縮寫,
host:主機端序,主機端序可能是大端序,也可能是小端序,視OS而定
net:網絡端序,網絡端序都是固定使用大端序
short:短整形
參數
hostshort:主機端序的端口號
返回值
該函數的調用永遠都是成功的,返回轉換后的端口號
htons的兄弟函數
htonl、ntohs、ltohs
htonl:與htons唯一的區別是,轉完的端口號時long
ntohs:htons的相反情況,網絡端序轉為主機端序
ntohl:htonl的相反情況
有關端口號的數值問題
三個范圍:0~1023、1024~49151、49152~65535。
0~1023:這個范圍的端口最好不要用,因為這個范圍的端口已經被世界公認的各種服務征用了,比如80就被web服務征用了,所以所有web服務器程序的端口都是固定的80。
1024~49151:自己實現服務器程序,建議使用這個范圍的端口號
49152~65535:用於自動分配的,一般客戶端程序不會綁定固定的ip和端口,因為客戶端的Ip和端口都是自動分配的,在自動分配端口時,所分配的就是49152~65535范圍的端口。
inet_addr
原型
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char *cp);
功能
功能有2個
1. 將字符串形式的Ip"192.168.1.105"(點分十進制),轉為IPV4的32位無符號整形數的ip
2. 將無符號整形數的ip,從主機端序轉為網絡端序
參數
cp:字符串形式的ip
返回值
永遠成功,返回網絡端序的、32位無符號整形數的ip。
listen
原型
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
功能
將套接字文件描述符,從主動文件描述符變為被動描述符,然后用於被動監聽客戶的連接。
不要因為listen有聽的意思,就想當然的認為,listen就是用於被動監聽客戶連接的函數,事實上真正用於被動監聽客戶連接的函數,並不是listen,而是其它函數。listen的作用僅僅只是用於將“套接字文件描述符”變成被動描述符,以供“監聽函數”用於被動監聽客戶連接而已。
參數
sockfd
socket所返回的套接字文件描述符。socket返回的“套接字文件描述符”默認是主動的,如果你想讓它變為被動的話,你需要自己調用listen函數來實現。
backlog
指定隊列的容量。這個隊列用於記錄正在連接,但是還沒有連接完成的客戶,一般將隊列容量指定為2、3就可以了。這個容量並沒有什么統一個設定值,一般來說只要小於30即可。
返回值
成功返回0,失敗返回-1,ernno被設置
|值 | 含義 | |------------|-------- | | EADDRINUSE |另一個套接字已經綁定在相同的端口上。 | |EBADF |參數sockfd不是有效的文件描述符。 | |ENOTSOCK |參數sockfd不是套接字。 | |EOPNOTSUPP |參數sockfd不是支持listen操作的套接字類型。|
主動描述符 & 被動描述符
主動描述符可以主動的向對方發送數據。
被動描述符只能被動的等別人主動想你發數據,然后再回答數據,不能主動的發送數據。
accept
原型
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
被動監聽客戶發起三次握手的連接請求,三次握手成功,即建立連接成功。
accept被動監聽客戶連接的過程,其實也被稱為監聽客戶上線的過程。對於那些只連接了一半,還未連接完成的客戶,會被記錄到未完成隊列中,隊列的容量由listen函數的第二個參數(backlog)來指定。服務器調用accept函數監聽客戶連接,而客戶端則是調用connect來連接請求的。一旦連接成功,服務器這邊的TCP協議會記錄客戶的IP和端口。
參數
sockefd
已經被listen轉為了被動描述符的“套接字文件描述符”,專門用於被動監聽客戶的連接。如果sockfd沒有被listen轉為被動描述符的話,accept是無法將其用來監聽客戶連接的。
有關套接字描述符的阻塞與非阻塞問題?
服務器程序調用socket得到“套接字文件描述符”時,如果socket的第2個參數type,沒有指定SOCK_NONBLOCK的話,“套接字文件描述符”默認就是阻塞的,所以accept使用它來監聽客戶連接時,如果沒有客戶請求連接的話,accept函數就會阻塞,直到有客戶連接位置。如果你不想阻塞,我們就可以在調用socket時,給type指定SOCK_NONBLOCK宏。
addrlen
第二參數addr的大小,不過要求給的是地址。
addr
用於記錄發起連接請求的那個客戶的IP和端口(port)。
如果服務器應用層需要用到客戶ip和端口的話,可以給accept指定第二個參數addr,以獲取TCP在連接時所自動記錄客戶IP和端口,如果不需要的就寫NULL。
addr為struct sockaddr類型,雖然下層(內核)實際使用的是struct sockaddr結構體,但是由於這個結構體用起來不方便,因此應用層會使用更加便於操作的結構體,比如使用TCP/IP協議族通信時,應用層使用的就是struct sockaddr_in這個更加方便操作的結構體。
所以我們應該定義struct sockaddr_in類型的addr,傳遞給accept函數時,將其強制轉為struct sockaddr即可,與我們講bind函數時的用法類似。
struct sockaddr_in clnaddr = {0}; int clnsize = sizeof(clnaddr) cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnsize);
返回值
成功:返回一個通信描述符,專門用於與該連接成功的客戶的通信,總之后續服務器與該客戶間正式通信,使用的就是accept返回的“通信描述符”來實現的。
失敗:返回-1,errno被設置
| 值 | 含義 | |----------------|-----------------------------------| |EBADF |非法的socket | |EFAULT |參數addr指針指向無法存取的內存空間 | |ENOTSOCK |參數s為一文件描述詞,非socket | |EOPNOTSUPP |指定的socket並非SOCK_STREAM | |EPERM |防火牆拒絕此連線 | |ENOBUFS |系統的緩沖內存不足 | |ENOMEM |核心內存不足 |
如何使用得到的客戶ip和端口
struct sockaddr_in clnaddr = {0}; int clnaddr_size = sizeof(clnaddr) cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size); printf("cln_port= %d, cln_addr=%s\n", ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr));
服務器調用read(recv)和write(send),收發數據,實現與客戶的通信
read和write的用法,在文件IO時已經介紹的非常清楚,我們這里着重介紹recv和send這兩個函數,recv和send其實和read和write差不多,它們的前三個參數都是一樣的,只是recv和send多了第四個參數。不管是使用read、write還是使用recv、send來實現TCP數據的收發,由於TCP建立連接時自動已經記錄下了對方的IP和端口,所以使用這些函數實現數據收發時,只需要指定通信描述符即可,不需要指定對方的ip和端口。
send
原型
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能
向對方發送數據
其實也可以使用sendto函數,相比send來說多了兩個參數,當sendto的后兩個參數寫NULL和0時,能完全等價於send。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, NULL, 0);
類似TCP這種面向連接的通信,我們一般使用send而不是使用sendto,因為sendto用起來有點麻煩。類似UDP這種不需要連接的通信,必須使用sendto,不能使用send。
參數
sockefd:
用於通信的通信描述符。不要因為名字寫的是sockfd,就認為一定是socket返回“套接字文件描述符”。在服務器這邊,accept返回的才是通信描述符,所以服務器調用send發送數據時,第一個參數應該寫accept所返回的通信描述符。
buf:
應用緩存,用於存放你要發送的數據。可以是任何你想發送的數據,比如結構體、int、float、字符、字符串等等。正規操作的話,應該使用結構體來封裝數據。
len:
buf緩存的大小
flags:
0:表示用不上flags,此時send是阻塞發送數據的。阻塞發送的意思就是,如果數據發送不成功會一直阻塞,直到被某信號中斷或者發送成功為止,不過一般來說,發送數據是不會阻塞的。當flags設置0時,send與write的功能完全一樣。
MSG_NOSIGNAL:send數據時,如果對方將“連接”關閉掉了,調用send的進程會被發送SIGPIPE信號,這個信號的默認處理方式是終止,所以收到這個信號的進程會被終止。如果給flags指定MSG_NOSIGNAL,表示當連接被關閉時不會產生該信號。從這里可看出,並不是只有寫管道失敗時才會產生SGIPIPE信號,網絡通信時也會產生這個的信號。
MSG_DONTWAIT:非阻塞發送
MSG_OOB:表示發送的是帶外數據
以上除了0以外,其它選項可以|操作,比如MSG_DONTWAIT | MSG_OOB。
返回值
成功返回發送的字節數,失敗返回-1,ernno被設置
recv
原型
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能
接收對方發送的數據。我們也可以使用rcvfrom函數,當recvfrom函數的最后兩個參數寫NULL和0時,與recv的功能完全一樣。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, NULL, 0);
參數
sockfd:通信文件描述符
buf:應用緩存,用於存放接收的數據
len:buf的大小
flags:
0:默認設置,此時recv是阻塞接收的,0是常設置的值。
MSG_DONTWAIT:非阻塞接收
MSG_OOB:接收的是帶外數據
返回值
成功返回接收的字節數,失敗返回-1,ernno被設置
調用close或者shutdown關閉TCP的連接
TCP斷開連接時,可以由客戶和服務器任何一方發起。調用close或者sutdown函數斷開連接時,四次握手的過程是由TCP自動完成的。
close
原型
#include <unistd.h> int close(int fd);
功能
關閉文件描述符
參數
fd:文件描述符
返回值
成功返回0,失敗返回-1,ernno被設置
close斷開連接的缺點
缺點1:會一次性將讀寫都關掉了。如果我只想關寫,但是讀打開着,或者只想關讀、但是寫打開着,close做不到。
缺點2:如果多個文件描述符指向了同一個連接時,如果只close關閉了其中某個文件描述符時,只要其它的fd還打開着,那么連接不會被斷開,直到所有的描述符都被close后才斷開連接。
出現多個描述指向同一個連接的原因可能兩個:
通過dup方式復制出其它描述符
子進程繼承了這個描述符,所以子進程的描述符也指向了連接
shutdown
原型
#include <sys/socket.h> int shutdown(int sockfd, int how);
功能
可以按照要求關閉連接,而且不管有多少個描述符指向同一個連接,只要調用shutdown去操作了其中某個描述符,連接就會被立即斷開。
參數
sokcfd:TCP服務器斷開連接時,使用的是accept所返回的文件描述符
how:如何斷開連接
SHUT_RD:只斷開讀連接
SHUT_WR:只斷開寫連接
SHUT_RDWR:讀、寫連接都斷開
返回值
成功返回0,失敗返回-1,ernno被設置
代碼演示
client.c

1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <strings.h> 6 #include <sys/types.h> /* See NOTES */ 7 #include <sys/socket.h> 8 #include <errno.h> 9 #include <sys/socket.h> 10 #include <netinet/in.h> 11 #include <arpa/inet.h> 12 #include <pthread.h> 13 #include <signal.h> 14 15 16 #define SPORT 5006 17 #define SIP "192.168.1.106" 18 19 /* 封裝應用層數據, 目前要傳輸的是學生數據 */ 20 //學生信息 21 typedef struct data 22 { 23 unsigned int stu_num; 24 char stu_name[50]; 25 }Data; 26 27 void print_err(char *str, int line, int err_no) 28 { 29 printf("%d, %s: %s\n", line, str, strerror(err_no)); 30 exit(-1); 31 } 32 33 int sockfd = -1; 34 35 void *pth_fun(void *pth_arg) 36 { 37 int ret = 0; 38 Data stu_data = {0}; 39 40 while(1) 41 { 42 bzero(&stu_data, sizeof(stu_data)); 43 44 ret = recv(sockfd, (void *)&stu_data, sizeof(stu_data), 0); 45 if(ret > 0) 46 { 47 printf("student number:%d\n", ntohl(stu_data.stu_num)); 48 printf("student name:%s\n", stu_data.stu_name); 49 } 50 else if(ret == -1) print_err("recv fail", __LINE__, errno); 51 52 } 53 } 54 55 void signal_fun(int signo) 56 { 57 if(SIGINT == signo) 58 { 59 /* 斷開連接 */ 60 //close(sockfd); 61 shutdown(sockfd); 62 63 exit(0); 64 } 65 } 66 67 int main(void) 68 { 69 int ret = 0; 70 71 signal(SIGINT, signal_fun); 72 73 /* 創建套接字文件,並指定使用TCP協議 74 * 對於客戶端的套接字文件描述符來說,直接用於通信 */ 75 sockfd = socket(PF_INET, SOCK_STREAM, 0); 76 if(sockfd == -1) print_err("socket fail", __LINE__, errno); 77 78 /* 調用connect,想服務器主動請求連接 */ 79 struct sockaddr_in seraddr = {0};//用於存放你要請求連接的那個服務器的ip和端口 80 81 seraddr.sin_family = AF_INET;//地址族 82 seraddr.sin_port = htons(SPORT);//服務器程序的端口 83 seraddr.sin_addr.s_addr = inet_addr(SIP);//服務器的ip,如果是跨網通信的話,就是服務器的公網ip 84 85 ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); 86 if(ret == -1) print_err("connect fail", __LINE__, errno); 87 88 pthread_t tid; 89 ret = pthread_create(&tid, NULL, pth_fun, NULL); 90 if(ret != 0) print_err("pthread_create fail", __LINE__, ret); 91 92 93 Data stu_data = {0}; 94 int tmp_num = 0; 95 while(1) 96 { 97 bzero(&stu_data, sizeof(stu_data)); 98 /* 封入學生學號 */ 99 printf("input student number\n"); 100 scanf("%d", &tmp_num); 101 stu_data.stu_num = htonl(tmp_num); 102 103 /* 封如學生名字 */ 104 printf("input student name\n"); 105 scanf("%s", stu_data.stu_name); 106 107 ret = send(sockfd, (void *)&stu_data, sizeof(stu_data), 0); 108 if(ret == -1) print_err("send fail", __LINE__, errno); 109 } 110 111 return 0; 112 }
server.c

1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <strings.h> 6 #include <sys/types.h> /* See NOTES */ 7 #include <sys/socket.h> 8 #include <errno.h> 9 #include <sys/socket.h> 10 #include <netinet/in.h> 11 #include <arpa/inet.h> 12 #include <pthread.h> 13 #include <signal.h> 14 15 16 #define SPORT 5006 17 #define SIP "192.168.1.106" 18 19 /* 封裝應用層數據, 目前要傳輸的是學生數據 */ 20 //學生信息 21 typedef struct data 22 { 23 unsigned int stu_num; 24 char stu_name[50]; 25 }Data; 26 27 void print_err(char *str, int line, int err_no) 28 { 29 printf("%d, %s: %s\n", line, str, strerror(err_no)); 30 exit(-1); 31 } 32 33 int cfd = -1;//存放與連接客戶通信的通信描述符 34 35 void signal_fun(int signo) 36 { 37 if(signo == SIGINT) 38 { 39 //斷開連接 40 //close(cfd); 41 shutdown(cfd, SHUT_RDWR); 42 43 exit(0); 44 } 45 } 46 47 48 /* 此線程接收客戶端的數據 */ 49 void *pth_fun(void *pth_arg) 50 { 51 int ret = 0; 52 Data stu_data = {0}; 53 54 while(1) 55 { 56 bzero(&stu_data, sizeof(stu_data)); 57 //ret = read(cfd, &stu_data, sizeof(stu_data)); 58 ret = recv(cfd, &stu_data, sizeof(stu_data), 0); 59 if(ret == -1) print_err("recv fail", __LINE__, errno); 60 else if(ret > 0) 61 { 62 printf("student number = %d\n", ntohl(stu_data.stu_num)); 63 printf("student name = %s\n", stu_data.stu_name); 64 } 65 } 66 } 67 68 int main(void) 69 { 70 int ret = -1; 71 int sockfd = -1; 72 73 74 signal(SIGINT, signal_fun); 75 76 /* 創建使用TCP協議通信的套接字文件 */ 77 sockfd = socket(PF_INET, SOCK_STREAM, 0); 78 if(sockfd == -1) print_err("socket fail", __LINE__, errno); 79 80 /* 調用Bind綁定套接字文件/ip/端口 */ 81 struct sockaddr_in saddr; 82 saddr.sin_family = AF_INET;//制定ip地址格式(地址族) 83 saddr.sin_port = htons(SPORT);//服務器端口 84 saddr.sin_addr.s_addr = inet_addr(SIP);//服務器ip 85 86 ret = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); 87 if(ret == -1) print_err("bind fail", __LINE__, errno); 88 89 /* 講主動的"套接字文件描述符"轉為被動描述符,用於被動監聽客戶的連接 */ 90 ret = listen(sockfd, 3); 91 if(ret == -1) print_err("listen fail", __LINE__, errno); 92 93 /* 調用accept函數,被動監聽客戶的連接 */ 94 struct sockaddr_in clnaddr = {0};//存放客戶的ip和端口 95 int clnaddr_size = sizeof(clnaddr); 96 97 cfd = accept(sockfd, (struct sockaddr *)&clnaddr, &clnaddr_size); 98 if(cfd == -1) print_err("accept fail", __LINE__, errno); 99 //打印客戶的端口和ip, 一定要記得進行端序轉換 100 printf("clint_port = %d, clint_ip = %s\n", ntohs(clnaddr.sin_port), inet_ntoa(clnaddr.sin_addr)); 101 102 /* 創建一個次線程,用於接受客戶發送的數據 */ 103 pthread_t tid; 104 ret = pthread_create(&tid, NULL, pth_fun, NULL); 105 if(ret != 0) print_err("pthread_create fail", __LINE__, ret); 106 107 Data stu_data = {0}; 108 int tmp_num; 109 while(1) 110 { 111 bzero(&stu_data, sizeof(stu_data)); 112 /* 獲取學生學號,但是需要講讓從主機端序轉為網絡端序 */ 113 printf("input student number\n"); 114 scanf("%d", &tmp_num); 115 stu_data.stu_num = htonl(tmp_num); 116 117 /* char的數據不需要進行端序的轉換 */ 118 printf("input student name\n"); 119 scanf("%s", stu_data.stu_name); 120 121 /* 發送數據 */ 122 //ret = write(cfd, (void *)stu_data, sizeof(stu_data)); 123 ret = send(cfd, (void *)&stu_data, sizeof(stu_data), 0); 124 if(ret == -1) print_err("send fail", __LINE__, errno); 125 } 126 127 128 return 0; 129 }