網絡編程——TCP編程


前言

使用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 }
View Code

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 }
View Code

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM