第一章 TCP/IP簡介
基本的C/S服務(wù)模型
網(wǎng)絡(luò)編程是指編寫的網(wǎng)絡(luò)通信程序可以與網(wǎng)絡(luò)上的其他程序進(jìn)行通信。
TCP/IP四層結(jié)構(gòu)
網(wǎng)絡(luò)接口層
網(wǎng)際層負(fù)責(zé)相鄰互聯(lián)網(wǎng)上的不同主機(jī)之間的通信启涯,主要包括IPv4、ICMP售睹、RIP、IGMP
傳輸層負(fù)責(zé)主機(jī)中兩個進(jìn)程之間的通信稍坯,主要包括TCP似芝、UDP
應(yīng)用層直接為用戶的應(yīng)用進(jìn)程提供服務(wù)
套接字編程就是應(yīng)用層到傳輸層的接口(API)
TCP協(xié)議介紹
TCP(傳輸控制協(xié)議)是TCP/IP體系中面向連接的運(yùn)輸層協(xié)議,它可以保證數(shù)據(jù)可靠的傳輸卸亮。
碼元比特(6位):分為6個標(biāo)志,置1表示有效
URG和緊急指針配合使用玩裙,發(fā)送緊急數(shù)據(jù)
ACK指出確認(rèn)字段是否有效
PSH接收方應(yīng)該盡快將這個報文提交給應(yīng)用層
RST重建連接
SYN同步序號用來請求建立連接
FIN用來釋放連接
TCP連接建立與終止
TCP是面向連接的協(xié)議兼贸。TCP連接的建立和釋放是每一次通信中必不可少的過程。
TCP連接的建立需要經(jīng)過三次數(shù)據(jù)傳輸吃溅。也就是三次握手溶诞。
- 服務(wù)器準(zhǔn)備好接收客戶的連接請求 -> socket、bind决侈、listen函數(shù)
- 客戶主動打開 -> connect函數(shù)螺垢,SYN分節(jié)(用來請求建立連接)
- 服務(wù)器收到客戶端發(fā)來的SYN分節(jié)后,必須發(fā)送ACK對其確認(rèn)赖歌,同時發(fā)送SYN分節(jié)給客戶端枉圃,表示接受客戶端建立連接的請求 -> SYN分節(jié),ACK(指出確認(rèn)字段是否有效)
- 客戶端發(fā)送ACK確認(rèn)服務(wù)器的SYN -> ACK(指出確認(rèn)字段是否有效)
- 連接建立成功
- 客戶端主動關(guān)閉連接 -> close函數(shù)庐冯,F(xiàn)IN分節(jié)
- 服務(wù)器收到FIN分節(jié)后執(zhí)行被動關(guān)閉孽亲,并關(guān)閉套接字 -> 發(fā)送ACK(對客戶端的FIN分進(jìn)行確認(rèn)),close函數(shù)展父,F(xiàn)IN分節(jié)
- 客戶端接受到FIN分節(jié)后返劲,發(fā)送ACK確認(rèn)分節(jié)后,徹底關(guān)閉連接
TCP連接中的分組交換
UDP協(xié)議介紹
UDP(用戶數(shù)據(jù)報協(xié)議)是面向無連接的服務(wù)栖茉,提供不可靠的數(shù)據(jù)傳輸篮绿。
第二章 套接字編程簡介
套接字基礎(chǔ)
套接字是一種網(wǎng)絡(luò)API(應(yīng)用程序編程接口),可以用它來開發(fā)網(wǎng)絡(luò)程序
套接字接口提供一種進(jìn)程間通信的方法衡载,使得在相同或不同的主機(jī)上的進(jìn)程能以相同的規(guī)范進(jìn)行雙向信息傳送
套接字接口是應(yīng)用層到傳輸層的接口
套接字類型
套接字類型是指創(chuàng)建套接字的應(yīng)用程序要使用的通信服務(wù)的類型。
最常用的幾種類型:
- SOCK_STREAM:流式套接字隙袁,提供面向連接痰娱、可靠的數(shù)據(jù)傳輸服務(wù)弃榨,數(shù)據(jù)是按字節(jié)流、按照順序收發(fā)梨睁,保證數(shù)據(jù)在傳輸過程中無丟失鲸睛、無冗余。
TCP支持該套接字
- SOCK_DGRAM:數(shù)據(jù)報套接字坡贺,提供面向無連接的服務(wù)官辈,數(shù)據(jù)收發(fā)無序,不能保證數(shù)據(jù)的準(zhǔn)確到達(dá)遍坟。
UDP支持該套接字
- SOCK_RAW:原始套接字拳亿。允許對低于傳輸層的協(xié)議或物理網(wǎng)絡(luò)直接訪問,例如可以接收和發(fā)送ICMP報(網(wǎng)絡(luò)層的協(xié)議)愿伴。常用于檢測新的協(xié)議肺魁。
套接字地址結(jié)構(gòu)
IPv4套接字地址結(jié)構(gòu)
#include <netinet/in.h>
typedef uint32_t in_addr_t; //無符號32位整數(shù),IPv4地址
typedef uint16_t in_port_t; //無符號16位整數(shù)隔节,TCP或UDP端口
typedef unsigned short sa_family_t; //套接字地址結(jié)構(gòu)的地址族 unsigned short 0~65535字節(jié)
struct in_addr{
in_addr_t s_addr; //s_addr成員存儲的是網(wǎng)絡(luò)字節(jié)序的32位IPv4地址
};
struct sockaddr_in{
uint8_t sin_len; //長度成員鹅经,存儲套接字地址結(jié)構(gòu)的長度(一般不設(shè)置)
sa_family_t sin_family; //sin_family是Internet地址族,在IPv4中是AF_INET
in_port_t sin_port; //端口號怎诫,以網(wǎng)絡(luò)字節(jié)序存儲
struct in_addr sin_addr; //是一個結(jié)構(gòu)瘾晃,該結(jié)構(gòu)中的成員存儲的才是IP地址
char sin_zero[8]; //未使用,置0
};
舉個例子:
struct sockaddr_in ser;
ser.sin_addr給出的是一個存放地址的結(jié)構(gòu)
ser.sin_addr.s_addr存儲的是地址中的內(nèi)容幻妓,也就是IP地址的值
IPv6套接字地址結(jié)構(gòu)
#include <netinet/in.h>
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t;
struct in6_addr{
uint8_t s6_addr[16];
};
struct sockaddr_in6{
uint8_t sin6_len; //長度成員
sa_family_t sin6_family; //Internet地址族蹦误,在IPv6中是AF_INET6
in_port_t sin6_port; //端口號,以網(wǎng)絡(luò)字節(jié)序存儲
uint32_t sin6_flowinfo; //低24位是流量標(biāo)號涌哲,下4位是優(yōu)先級胖缤,再下4位保留
struct in6_addr sin6_addr; //in6_addr結(jié)構(gòu)中的s6_addr成員,存儲的是網(wǎng)絡(luò)字節(jié)序的128位IPv6地址
};
兩種套接字地址結(jié)構(gòu)的比較
通用套接字地址結(jié)構(gòu)
套接字地址結(jié)構(gòu)作為參數(shù)傳遞給任一個套接字函數(shù)時阀圾,通常通過指針
來傳遞
當(dāng)套接字函數(shù)取得此參數(shù)時哪廓,參數(shù)中可能存放的是來自所支持的任何協(xié)議族的地址結(jié)構(gòu)。因此在調(diào)用套接字函數(shù)時初烘,需要將指向特定協(xié)議的地址結(jié)構(gòu)的指針類型轉(zhuǎn)換成指向通用的地址結(jié)構(gòu)的指針涡真。
通用套接字地址結(jié)構(gòu)如下:
#include <sys/socket.h>
struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
套接字基本函數(shù)
字節(jié)排序函數(shù)
廣域網(wǎng)規(guī)定的網(wǎng)絡(luò)字節(jié)序采用大端字節(jié)序
- 小端字節(jié)序:將低序字節(jié)存儲在起始地址
- 大端字節(jié)序:將高序字節(jié)存儲在起始地址
將某給定主機(jī)所使用的字節(jié)序稱為主機(jī)字節(jié)序。為了使采用不同字節(jié)序的主機(jī)能夠互相通信肾筐,TCP/IP協(xié)議規(guī)定了網(wǎng)絡(luò)字節(jié)序哆料。
所有主機(jī)或路由器在發(fā)送IP數(shù)據(jù)包之前要首先將相應(yīng)的信息轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序。相應(yīng)的吗铐,在接收數(shù)據(jù)包后东亦,要將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換成主機(jī)字節(jié)序。
主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的相互轉(zhuǎn)換唬渗,要用到以下四個函數(shù):
#include <netinet/in.h>
uint16_t htons(uint16_t hosts);
uint32_t htonl(uint32_t hostl);
uint16_t ntohs(uint16_t nets);
uint32_t ntohl(uint32_t netl);
在上述四個函數(shù)中典阵,h代表主機(jī)host奋渔,n代表網(wǎng)network,s代表短整型short壮啊,l代表長整型long
- htons:將16位的短整型數(shù)嫉鲸,從主機(jī)字節(jié)序轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
- htonl:將32位的長整型數(shù),從主機(jī)字節(jié)序轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
- ntohs:將16位的短整型數(shù)歹啼,從網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換成主機(jī)字節(jié)序
- ntohl:將32位的長整型數(shù)玄渗,從網(wǎng)路字節(jié)序轉(zhuǎn)換成主機(jī)字節(jié)序
字節(jié)操縱函數(shù)
以字母b(byte)打頭
#include <string.h>
void bzero(void *dest, size_t len);
//bzero函數(shù)將目標(biāo)中指定數(shù)目的字節(jié)置為0,經(jīng)常用此函數(shù)來對套接字地址結(jié)構(gòu)進(jìn)行初始化
void bcopy(const void *src, void *dest, size_t len);
//bcopy函數(shù)將指定數(shù)目的字節(jié)從源拷貝到目標(biāo)
int bcmp(const void *src, void *dest, size_t len);
//bcmp函數(shù)比較源和目標(biāo)兩個字符串狸眼,若相同返回值為0藤树,否則返回非0值
以mem(memory)打頭
void *memset(void *dest, int x, size_t len);
//將目標(biāo)指定數(shù)目的字節(jié)置為值x
void *memcpy(void *dest, const void *src, size_t len);
//將指定數(shù)目的字節(jié)從源拷貝到目標(biāo)
int memcmp(const void *str1, const void *str2, size_t len);
//比較兩個字符串,若相同返回值為0份企,否則返回非0值也榄。如果str1所指字節(jié)大于str2所指字節(jié),則返回值大于0司志,否則返回值小于0
IP地址轉(zhuǎn)換函數(shù)
用于字符串的IP和二進(jìn)制值的IP相互轉(zhuǎn)換
#include <arpa/inet.h>
in_addr_t inet_addr(const char *str);
int inet_aton(const char *str, struct in_addr *numstr);
char *inet_ntoa(struct in_addr inaddr);
a代表ASCII串甜紫,n代表數(shù)值格式,是存在于套接字地址結(jié)構(gòu)中的二進(jìn)制值骂远。
inet_addr函數(shù)
將字符串形式的IP地址轉(zhuǎn)換成32位二進(jìn)制值的IP地址囚霸。str指向字符串形式的IP地址。函數(shù)調(diào)用成功激才,返回值為32位二進(jìn)制值的IP地址拓型。
inet_aton函數(shù)
將字符串形式的IP地址轉(zhuǎn)換成32位二進(jìn)制值的IP地址。str指向字符串形式的IP地址瘸恼。numstr指向轉(zhuǎn)換后的32位網(wǎng)絡(luò)字節(jié)序的IP地址劣挫。如果成功返回1,否則返回0
inet_ntoa函數(shù)
將一個32位網(wǎng)絡(luò)字節(jié)序的二進(jìn)制值的IP地址轉(zhuǎn)換成相應(yīng)的點(diǎn)分十進(jìn)制的IP地址东帅。這個函數(shù)的參數(shù)是一個結(jié)構(gòu)压固,而不是指向結(jié)構(gòu)的指針。該函數(shù)的返回值所指向的串留在靜態(tài)內(nèi)存中靠闭,所以函數(shù)是不可重入的
#include <arpa/inet.h>
int inet_pton(int family, const char *str, void *numstr);
const char *inet_ntop(int family, const void *numstr, char *str, size_t len);
p代表地址的表達(dá)格式是ASCII串帐我。n代表數(shù)值格式,是存在于套接字地址結(jié)構(gòu)中的二進(jìn)制值愧膀。
這兩個函數(shù)中的family參數(shù)拦键,指的是操作地址的地址族,IPv4是AF_INET檩淋,IPv6是AF_INET6
inet_pton函數(shù)
將指針str所指的字符串形式的IP地址芬为,轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序的二進(jìn)制值的IP地址,并用指針numstr存儲。如果成功返回1媚朦,如果對于指定的family輸入的字符串不是一個有效的表達(dá)格式捡絮,則返回值為0,出錯返回-1
inet_ntop函數(shù)
將numstr所指的二進(jìn)制值的IP地址轉(zhuǎn)換成字符串形式的IP地址莲镣,并用指針str存儲。參數(shù)len是目標(biāo)的大小涎拉,為了避免函數(shù)溢出其調(diào)用者的緩沖區(qū)瑞侮。
五個地址轉(zhuǎn)換函數(shù)
isfdtype函數(shù)
用于測試某個描述符是不是給定的類型
#include <sys/stat.h>
int isfdtype(int fd, int fdtype);
isfdtype函數(shù):測試描述符fd是不是fdtype參數(shù)指定的類型
為了測試描述符是否是套接字描述符,fdtype參數(shù)應(yīng)設(shè)為S_IFSOCK鼓拧。例如:
isfdtype(sockfd, S_IFSOCK);
值-結(jié)果參數(shù)
當(dāng)把套接字地址結(jié)構(gòu)傳遞給套接字函數(shù)時半火,總是通過指針來傳遞的,即傳遞的是一個指向套接字地址結(jié)構(gòu)的指針季俩,結(jié)構(gòu)的長度也可用參數(shù)傳遞钮糖。
從進(jìn)程到內(nèi)核傳遞套接字地址結(jié)構(gòu)的函數(shù): bind、connect酌住、sendto
在這三個函數(shù)的參數(shù)中都含有兩個相似的參數(shù)店归,分別是指向套接字地址結(jié)構(gòu)的指針及該地址結(jié)構(gòu)的大小。例如:
struct sockaddr_in ser;
bind(sockfd, (struct sockaddr *)&ser, sizeof(ser));
這里的bind函數(shù)將套接字地址結(jié)構(gòu)和結(jié)構(gòu)大小都傳遞給了內(nèi)核酪我,所以進(jìn)程到內(nèi)核拷貝的數(shù)據(jù)長度是確定的
從內(nèi)核到進(jìn)程傳遞套接字地址結(jié)構(gòu)的函數(shù): accept消痛、recvfrom、getsockname都哭、getpeername
這四個函數(shù)包含的兩個參數(shù)分別是指向套接字地址結(jié)構(gòu)的指針和指向套接字地址結(jié)構(gòu)大小的指針秩伞。例如:
struct sockaddr_in client;
socklen_t len;
len = sizeof(client);
accept(listenfd, (struct sockaddr *)&client, &len);
由于accept函數(shù)中存放套接字地址結(jié)構(gòu)長度的參數(shù)是個指針,那么在函數(shù)調(diào)用時欺矫,結(jié)構(gòu)長度是個值纱新,這個值告訴內(nèi)核該結(jié)構(gòu)的長度,使內(nèi)核在寫這個結(jié)構(gòu)時不會越界穆趴。而當(dāng)函數(shù)返回時脸爱,結(jié)構(gòu)大小的值發(fā)生了變化,變成了內(nèi)核在此結(jié)構(gòu)中確切存儲的數(shù)據(jù)長度毡代。進(jìn)程可以通過這個值得到內(nèi)核寫入多少信息到這個結(jié)構(gòu)中阅羹。這種在參數(shù)傳遞的過程中,其值發(fā)生變化了的參數(shù)稱為值-結(jié)果參數(shù)教寂,內(nèi)核和進(jìn)程間的兩種傳遞參數(shù)的方式如下圖:
第三章 基本TCP套接字編程
3.1 TCP套接字編程
使用TCP套接字編程可以實(shí)現(xiàn)基于TCP/IP協(xié)議的面向連接的通信捏鱼,它分為服務(wù)器和客戶端
兩部分:
TCP套接字編程中,服務(wù)器端實(shí)現(xiàn)的步驟:
- 使用socket()函數(shù)創(chuàng)建套接字
- 使用bind函數(shù)為創(chuàng)建的套接字綁定到指定的地址結(jié)構(gòu)
- listen()函數(shù)設(shè)置套接字為監(jiān)聽模式酪耕,使服務(wù)器進(jìn)入被動打開的狀態(tài)
- 接受客戶端的連接請求导梆,建立連接
- 接收、應(yīng)答客戶端的數(shù)據(jù)請求
- 終止連接
客戶端實(shí)現(xiàn)的步驟:
- 使用socket()函數(shù)創(chuàng)建套接字
- 調(diào)用connect()函數(shù)建立一個與TCP服務(wù)器的連接
- 發(fā)送數(shù)據(jù)請求,接收服務(wù)器的數(shù)據(jù)應(yīng)答
- 終止連接
socket函數(shù)
為了執(zhí)行網(wǎng)絡(luò)I/O看尼,無論是服務(wù)器還是客戶端递鹉,首先必須調(diào)用socket函數(shù),產(chǎn)生TCP套接字藏斩,作為TCP通信的傳輸端點(diǎn)
#include <sys/socket.h>
int socket(int family, int type, int protocol);
socket函數(shù)中family參數(shù)指明協(xié)議族躏结。type參數(shù)指明產(chǎn)生套接字的類型。protocol參數(shù)是協(xié)議標(biāo)志狰域,一般在調(diào)用socket函數(shù)時將其置為0媳拴,但如果是原始套接字,就需要為protocol指定一個常值兆览。
該函數(shù)調(diào)用成功屈溉,返回一個小的非負(fù)的整數(shù)值,它與文件描述符類似抬探,這里稱之為套接字描述符
子巾,簡稱套接字
,之后的I/O操作都由該套接字完成小压。如果函數(shù)調(diào)用失敗线梗,則返回-1。
family參數(shù)指明的協(xié)議族怠益,確定了socket使用的協(xié)議類型缠导,值通常為:
- AF_INET: IPv4協(xié)議
- AF_INET6: IPv6協(xié)議
- AF_ROUTE: 路由套接口
type參數(shù)指明產(chǎn)生套接字的類型,它常用的值包括:
- SOCK_STREAM: 字節(jié)流套接口溉痢,TCP使用的是這種格式
- SOCK_DGRAM: 數(shù)據(jù)報套接口僻造,UDP使用的是這種形式
- SOCK_RAW: 原始套接口
并不是所有的family和type的組合都有效,下表中Yes表示組合有效孩饼,No表示組合無效:
調(diào)用sock函數(shù)的代碼如下:
#include <sys/socket.h>
......
int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
//handle exception
......
}
connect函數(shù)
connect函數(shù)用于激發(fā)TCP的三次握手過程髓削。建立與遠(yuǎn)程服務(wù)器的連接
TCP客戶端使用connect函數(shù)來配置套接字,建立一個TCP服務(wù)器的連接
connect函數(shù)如下:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd參數(shù)是由socket函數(shù)返回的套接字描述符
addr參數(shù)是指向服務(wù)器的套接字地址結(jié)構(gòu)的指針镀娶,如果是IPv4地址立膛,server指向的就是一個sockaddr_in地址結(jié)構(gòu),在進(jìn)行connect函數(shù)調(diào)用時梯码,必須將sockaddr_in結(jié)構(gòu)轉(zhuǎn)換成通用地址結(jié)構(gòu)sockaddr宝泵。最后一個參數(shù)addrlen是該套接字地址結(jié)構(gòu)的大小。
調(diào)用成功返回0轩娶,出錯則返回-1
如果描述符是TCP套接字儿奶,調(diào)用函數(shù)connect就是建立一個TCP的連接,只在連接建立成功或者出錯時該函數(shù)才返回鳄抒,返回的錯誤有如下幾種情況:
- 如果客戶沒有收到SYN分節(jié)的響應(yīng)闯捎,返回ETIMEDOUT椰弊,這可能需要重發(fā)若干次SYN
- 如果對客戶的SYN的響應(yīng)是RST,則表明該服務(wù)器主機(jī)在指定的端口上沒有進(jìn)程在等待與之相連瓤鼻”妫客戶端馬上返回錯誤ECONNREFUSED
- 如果客戶發(fā)出的SYN在中間路由器上引發(fā)一個目的地不可達(dá)ICMP錯誤,客戶端內(nèi)核保存此消息茬祷,并按第一種情況清焕,連續(xù)重傳SYN,直到規(guī)定時間的超時時間祭犯,對方仍沒有響應(yīng)耐朴,則返回保存的消息(即ICMP錯誤)EHOSTUNREACH或ENETUNREACH錯誤返回給進(jìn)程
對于TCP連接的狀態(tài),connect導(dǎo)致客戶端從CLOSED狀態(tài)轉(zhuǎn)到了SYN_SENT狀態(tài)盹憎。
若建立連接成功,也就是connect調(diào)用成功铐刘,狀態(tài)會再變到ESTABLISHED狀態(tài)陪每。若函數(shù)connect調(diào)用失敗,則套接字不能再使用镰吵,必須關(guān)閉檩禾。如果想繼續(xù)向服務(wù)器發(fā)起建立連接的請求,就需要重新調(diào)用socket函數(shù)疤祭,生成新的套接字
調(diào)用connect函數(shù)的代碼如下:
#include <sys/socket.h>
......
int socked;
struct sockaddr_in server;
......
bzero(&server, sizeof(server));//為套接字地址結(jié)構(gòu)server設(shè)置初始值0
server.sin_family = AF_INET; //為套接字地址結(jié)構(gòu)中的成員賦值
server.sin_port = htons(1234); //為套接字地址結(jié)構(gòu)中的成員賦值盼产,端口號為1234
server.sin_addr.s_addr = inet_addr("127.0.0.1");
//為套接字地址結(jié)構(gòu)中的成員賦值,127.0.0.1是客戶端要建立連接的服務(wù)器的IP地址
if(connect(sockfd, (struct sockaddr *)&server, sizeof(server) == -1)
//調(diào)用connect函數(shù)勺馆,與服務(wù)器建立連接戏售,connect函數(shù)第二個參數(shù)為將IPv4的套接字地址結(jié)構(gòu)強(qiáng)制轉(zhuǎn)換為通用地址結(jié)構(gòu)
{
//handle exception //如果調(diào)用connect函數(shù)失敗,連接失敗的異常處理
......
}
......
bind函數(shù)
綁定函數(shù)bind的作用就是為調(diào)用socket函數(shù)產(chǎn)生的套接字分配一個本地協(xié)議地址草穆,建立地址與套接字的對應(yīng)關(guān)系
對于網(wǎng)際協(xié)議灌灾,協(xié)議地址包括32位的IPv4地址或128位的IPv6地址和16位的UDP或TCP的端口號
對于綁定操作,地址信息必須是唯一的悲柱,在實(shí)際應(yīng)用中锋喜,通過綁定的端口號來保證地址的唯一性
bind函數(shù)如下:
#include <sys/socket.h>
int bind(int socked. const struct sockaddr *server, socklen_len addrlen);
參數(shù)sockfd是套接字函數(shù)返回的套接字描述符
參數(shù)server是指向特定于協(xié)議的地址結(jié)構(gòu)的指針,指定用于通信的本地協(xié)議地址
參數(shù)addrlen指定了該套接字地址結(jié)構(gòu)的長度
如果調(diào)用成功返回0豌鸡,調(diào)出錯返回-1嘿般,并置錯誤號errno
對于綁定的套接字地址結(jié)構(gòu),可以指定端口號或IP地址中的任意一個涯冠,可以兩個都指定炉奴,也可以一個也不指定。如果不綁定任何端口蛇更,當(dāng)調(diào)用connect或listen時盆佣,內(nèi)核會為套接字選擇一個臨時的端口往堡。
進(jìn)程如果綁定了一個特定的本地IP地址到它的套接字上,對于TCP客戶端共耍,這就為在此套接字上發(fā)送的IP數(shù)據(jù)報分配了源IP地址虑灰。對于TCP服務(wù)器端,這就限制了該套接字只接收目的地址為此IP地址的客戶連接
bind函數(shù)的代碼如下:
#include <sys/socket.h>
......
int sockfd;
int port = 1234;//bind的端口號為1234
struct sockaddr_in server;
......
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址是一個通配地址穆咐,由內(nèi)核選擇IP地址。關(guān)于INADDR_ANY見上圖
if(bind(sockfd, (struct sockaddr *)&server, sizeof(server) == -1))
{
//handle exception
......
}
......
listen函數(shù)
當(dāng)調(diào)用函數(shù)socket創(chuàng)建一個套接字時字旭,默認(rèn)情況下它是一個主動套接字对湃,也就是一個將調(diào)用connect函數(shù)發(fā)起連接的客戶端套接字。所以對于TCP服務(wù)器遗淳,在綁定操作后拍柒,必須要調(diào)用listen函數(shù),將這個未連接的套接字轉(zhuǎn)換成被動套接字屈暗,監(jiān)聽有無客戶要連接拆讯,進(jìn)入被動接受連接請求狀態(tài)。
在調(diào)用listen函數(shù)后养叛,服務(wù)器的狀態(tài)從CLOSED轉(zhuǎn)換到了LISTEN狀態(tài)
listen函數(shù)如下:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
參數(shù)sockfd是要設(shè)置的描述符
參數(shù)backlog規(guī)定了請求隊列中的最大連接個數(shù)种呐,它對隊列中等待服務(wù)請求的數(shù)目進(jìn)行了限制,如果一個服務(wù)請求到來時弃甥,輸入隊列已滿爽室,該套接字將拒絕連接請求。
函數(shù)調(diào)用成功返回0淆攻,出錯返回-1阔墩,并置errno值
listen函數(shù)代碼如下:
#include <sys/socket.h>
......
int sockfd;
int BACKLOG = 5; //設(shè)置監(jiān)聽的最大連接數(shù)為5
......
if(listen(sockfd, BACKLOG) == -1) //調(diào)用listen函數(shù),將sockfd描述符設(shè)置為監(jiān)聽描述符
{
//handle exception //當(dāng)listen函數(shù)調(diào)用失敗時的異常處理
......
}
......
accept函數(shù)
accept函數(shù)使服務(wù)器接受客戶端的連接請求
它將完成隊列中的隊頭條目返回給進(jìn)程瓶珊,并產(chǎn)生一個新的套接字描述符——已連接套接字
當(dāng)已完成隊列為空戈擒,則進(jìn)程睡眠,直到有已完成連接到達(dá)時艰毒。
accept函數(shù)如下:
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t *addrlen);
listenfd函數(shù)是由socket函數(shù)產(chǎn)生的套接字描述符筐高,在調(diào)用accept函數(shù)前,已經(jīng)調(diào)用listen函數(shù)將此套接字變成了監(jiān)聽套接字
client和addrlen參數(shù)用來返回連接對方的套接字地址結(jié)構(gòu)和對應(yīng)的結(jié)構(gòu)長度
addrlen參數(shù)是一個值—結(jié)果參數(shù)丑瞧,調(diào)用前柑土,將addrlen指針?biāo)傅闹抵脼閏lient所指的套接字地址結(jié)構(gòu)的長度。函數(shù)返回時绊汹,此整數(shù)值變?yōu)閮?nèi)核寫入此套接字地址結(jié)構(gòu)的準(zhǔn)確字節(jié)數(shù)稽屏。
函數(shù)調(diào)用成功時,可以得到三個值:
- 一個是accept函數(shù)的返回值西乖,
已連接套接字描述符
狐榔。已連接套接字描述符是內(nèi)核為每個被接受的客戶都分別創(chuàng)建一個坛增。用完則關(guān)閉。監(jiān)聽描述符負(fù)責(zé)接收客戶的連接請求薄腻,而已連接描述符負(fù)責(zé)與對應(yīng)的客戶進(jìn)行數(shù)據(jù)傳輸
- 由client參數(shù)返回客戶端的協(xié)議地址收捣,包括IP地址和端口號等
- 由addrlen參數(shù)返回客戶端地址結(jié)構(gòu)的大小
如果對客戶的協(xié)議地址和地址結(jié)構(gòu)的長度不感興趣,可以將client和addrlen兩個參數(shù)都設(shè)為空指針庵楷。
如果函數(shù)調(diào)用失敗罢艾,accept函數(shù)將返回-1,并置errno值尽纽。調(diào)用accept函數(shù)的代碼如下:
#include <sys/socket.h>
......
int listenfd, connfd;
//定義了兩個套接字描述符咐蚯,一個是監(jiān)聽套接字描述符,一個是已連接套接字描述符
struct sockaddr_in client;
socklen_t addrlen;
addrlen = sizeof(client); //得到client當(dāng)前的長度
......
connfd = accept(listenfd, (struct sockaddr *)&client, &addrlen);
//調(diào)用accept函數(shù)弄贿,接收連接請求春锋,返回已連接套接字描述符
//與服務(wù)器連接的客戶端的協(xié)議地址可以通過參數(shù)client得到,addrlen返回內(nèi)核寫入client結(jié)構(gòu)體中的準(zhǔn)確字節(jié)數(shù)
if(connfd == -1){
//handle exception
......
}
......
數(shù)據(jù)傳輸函數(shù)
服務(wù)端和客戶端連接建立成功后差凹,就可以進(jìn)行雙向的數(shù)據(jù)傳輸期奔。服務(wù)端和客戶端使用各自的套接字描述符進(jìn)行讀寫操作
write函數(shù)
write()函數(shù)用于數(shù)據(jù)的發(fā)送,如下:
#include <unistd.h>
int write(int sockfd, char *buf, int len);
參數(shù)sockfd是套接字描述符直奋。對于服務(wù)器是accept()函數(shù)返回的已連接套接字描述符。對于客戶端是調(diào)用socket()函數(shù)返回的套接字描述符施禾。參數(shù)buf是指向一個用于發(fā)送信息的數(shù)據(jù)緩沖區(qū)脚线。len指明傳送數(shù)據(jù)緩沖區(qū)的大小。
函數(shù)滴啊用成功返回大于0的整數(shù)弥搞,也就是發(fā)送的字節(jié)數(shù)邮绿。出錯則返回-1。
read函數(shù)
用于數(shù)據(jù)的接收
#include <unistd.h>
int read(int sockfd, char *buf, int len);
參數(shù)sockfd是套接字描述符攀例。對于服務(wù)器是accept()函數(shù)返回的已連接套接字描述符船逮。對于客戶端是調(diào)用socket()函數(shù)返回的套接字描述符。參數(shù)buf是指向一個用于接收信息的數(shù)據(jù)緩沖區(qū)粤铭。len指明傳送數(shù)據(jù)緩沖區(qū)的大小挖胃。
函數(shù)滴啊用成功返回大于0的整數(shù),也就是接收的字節(jié)數(shù)梆惯。出錯則返回-1酱鸭。
調(diào)用read()函數(shù)的代碼如下:
#include <unistd.h>
#include <sys/socket.h>
#define MAXDATASIZE 100 //定義接收信息的數(shù)據(jù)緩沖區(qū)的長度
......
int num, connfd;
char buf[MAXDATASIZE];
......
if( (num = read(connfd, buf, MAXDATASIZE)) > 0){ //接收數(shù)據(jù),整數(shù)num返回接收的字節(jié)數(shù)
//handle data //處理收到的數(shù)據(jù)
......
}
......
send函數(shù)
用于數(shù)據(jù)的發(fā)送操作
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
前三個參數(shù)與write相同垛吗,參數(shù)flags是傳輸控制標(biāo)志凹髓,其值定義如下圖:
recv函數(shù)
用于數(shù)據(jù)的發(fā)送操作
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
調(diào)用recv函數(shù)的代碼如下:
#include <unistd.h>
#include <sys/socket.h>
#define MAXDATASIZE 100
......
int num, connfd;
char buf[MAXDATASIZE];
......
if( (num = recv(connfd, buf, MAXDATASIZE)) > 0){ //接收數(shù)據(jù),整數(shù)num返回接收的字節(jié)數(shù)
//handle data //處理收到的數(shù)據(jù)
......
}
......
TCP套接字編程實(shí)例
程序?qū)崿F(xiàn)的功能是:
- 客戶根據(jù)用戶提供的IP地址怯屉,連接到相應(yīng)的服務(wù)器
- 服務(wù)器等待客戶的連接蔚舀,一旦連接成功饵沧,則顯示客戶的IP地址、端口號赌躺,并向客戶發(fā)送字符串
- 客戶接受服務(wù)器發(fā)送的信息并顯示
TCP服務(wù)器端程序如下(server.c):
#include <stdio.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h> //通用套接字地址結(jié)構(gòu)
#include <netinet/in.h> //IPv4套接字地址結(jié)構(gòu)
#include <arpa/inet.h> //IP地址轉(zhuǎn)換函數(shù)
#include <string.h>
#include <stdlib.h>
#define PORT 1234 //客戶端與服務(wù)器的端口要對應(yīng)
#define BACKLOG 1//listen函數(shù)中的參數(shù)狼牺,此參數(shù)規(guī)定了請求隊列中的最大連接個數(shù),由于本例不是并發(fā)服務(wù)器寿谴,所以最大允許連接的數(shù)量BACKLOG定義為1
main()
{
int listenfd, connectfd/*已連接套接字描述符*/;
struct sockaddr_in server;//套接字地址結(jié)構(gòu)
struct sockaddr_in client;
socklen_t addrlen;
/*Create TCP socket*/
if ((listenfd=socket(AF_INET, SOCK_STREAM, 0)) == -1)
//創(chuàng)建TCP套接字,socket()函數(shù)第二個參數(shù)為字節(jié)流接口锁右。如果出錯打印錯誤信息。
{
perror("socket() error.");
exit(-1);
}
int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//以上兩行設(shè)置套接字選項SO_REUSEADDR讶泰,即地址重用選項咏瑟。
//由于系統(tǒng)默認(rèn)是只允許一個套接字綁定一個特定的協(xié)議地址上,并且當(dāng)該套接字關(guān)閉后痪署,系統(tǒng)仍不允許在該地址上綁定其他套接字码泞。
//如果去掉這兩行,程序運(yùn)行時產(chǎn)生的錯誤信息為:"Bind() error:Address already in use"
bzero(&server, sizeof(server));//初始化server套接字地址結(jié)構(gòu)狼犯,初始值為0
server.sin_family = AF_INET;//為套接字地址結(jié)構(gòu)中的成員賦值
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1)
//調(diào)用bind()函數(shù)將套接字描述符與server套接字地址結(jié)構(gòu)中的協(xié)議地址綁定
{
perror("bind() eror");
exit(1);
}
if (listen(listenfd, BACKLOG) == -1) //listen()函數(shù)將listenfd描述符設(shè)置為監(jiān)聽套接字余寥,等待客戶連接
{
perror("listen() error.\n");
exit(1);
}
addrlen = sizeof(client);//得到client當(dāng)前的長度
if ((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
//接受客戶端連接,將客戶的地址信息存放在client地址結(jié)構(gòu)中悯森,&addrlen為內(nèi)核寫入client結(jié)構(gòu)體中的準(zhǔn)確字節(jié)數(shù)
{
perror("accept() error\n");
exit(1);
}
printf("You got a connection from client's ip is %s, port is %d\n", inet_ntoa(client.sin_addr), htons(client.sin_port));
//顯示客戶的IP地址和端口號宋舷,通過inet_ntoa()函數(shù)將IP地址轉(zhuǎn)換成可顯示的ASCII串,通過htons()函數(shù)將端口號轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
send(connectfd, "Welcome\n",8,0);//發(fā)送Welcome字符串給客戶端
close(connectfd);//先關(guān)閉已連接套接字瓢姻,再關(guān)閉監(jiān)聽套接字
close(listenfd);
return 0;
}
TCP客戶端程序如下(client.c):
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#define PORT 1234 //服務(wù)器的端口與客戶端的端口對等
#define MAXDATASIZE 100 //這里的緩沖區(qū)采用靜態(tài)方式分配
int main(int argc, char *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in server;
if (argc != 2) //檢查用戶的輸入祝蝠。如果用戶輸入不正確,提示用戶正確的輸入方式
{
printf("Usage:%s <IP Address>\n", argv[0]);
exit(1);
}
//通過用戶輸入的點(diǎn)分十進(jìn)制形式的IP地址幻碱,獲得服務(wù)器的相關(guān)地址信息
if ((he = gethostbyname(argv[1])) == NULL)
{
printf("gethostbyname() error\n");
exit(1);
}
//調(diào)用socket()函數(shù)產(chǎn)生套接字描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("socket() error\n");
exit(1);
}
//初始化服務(wù)器的地址結(jié)構(gòu)绎狭,并為地址結(jié)構(gòu)的成員賦值
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr);
//調(diào)用connect()函數(shù)連接到服務(wù)器server
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{
printf("connect() error\n");
exit(1);
}
//接受服務(wù)器發(fā)過來的字符串,并保存在buf中褥傍。接收的真正字節(jié)數(shù)被存儲在num中
if ((num = recv(sockfd, buf, MAXDATASIZE,0)) == -1)
{
printf("recv() error\n");
exit(1);
}
buf[num-1] = '\0'; //以\0標(biāo)志字符串的結(jié)束
printf("server message: %s\n", buf);//顯示從服務(wù)器接收到的buf中的信息
close(sockfd); //關(guān)閉套接字
}
代碼運(yùn)行截圖:
第四章 基本UDP套接字編程
UDP套接字編程
UDP套接字編程中儡嘶,服務(wù)器端實(shí)現(xiàn)的步驟:
- 使用socket()函數(shù)創(chuàng)建套接字
- 為創(chuàng)建的套接字綁定到指定的地址結(jié)構(gòu)
- 等待接受客戶端的數(shù)據(jù)請求
- 處理客戶端的請求
- 向客戶端發(fā)送應(yīng)答數(shù)據(jù)
- 關(guān)閉套接字
客戶端實(shí)現(xiàn)的步驟:
- 使用socket()函數(shù)創(chuàng)建套接字
- 發(fā)送數(shù)據(jù)請求給服務(wù)器
- 等待接收服務(wù)器的數(shù)據(jù)應(yīng)答
- 關(guān)閉套接字
recvfrom函數(shù)
此函數(shù)用于接收數(shù)據(jù),函數(shù)中藥指明源地址
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, size_t *addrlen);
//前三個參數(shù)分別為:調(diào)用socket函數(shù)生成的描述符恍风、指向讀入緩沖區(qū)的指針蹦狂、讀入的字節(jié)數(shù)
//flags參數(shù)是傳輸控制標(biāo)志,其值通常為0朋贬,代表所做的操作與read相同邦鲫,還有MSG_OOB, MSG_PEEK
//from返回與之通信的對方的套接字地址結(jié)構(gòu)科盛,告訴用戶接收到的數(shù)據(jù)報來自于誰
//addrlen是一個指向整數(shù)值的指針(值-結(jié)果參數(shù))婿滓,存儲數(shù)據(jù)發(fā)送者的套接字地址結(jié)構(gòu)的字節(jié)數(shù)
//如果from或addrlen兩者之一要為空邻薯,必須同時設(shè)為空
函數(shù)調(diào)用成功的返回值為接收到數(shù)據(jù)的長度(以字節(jié)為單位),也就是接收的數(shù)據(jù)報中用戶數(shù)據(jù)的總量御滩。調(diào)用失敗返回-1鸥拧,并置errno
調(diào)用recvfrom函數(shù)代碼如下:
#include <sys/types.h>
#include <sys/socket.h>
#define MAXDATASIZE 100
......
int num, sockfd;
socklen_t addrlen;
sockaddr_in peer_addr;
char buf[MAXDATASIZE];
......
addrlen = sizeof(peer_addr);
while(1)
{
num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&peer_addr, &addrlen);
//用recvfrom函數(shù)接收數(shù)據(jù)党远,將接收到的數(shù)據(jù)保存在buf中,整數(shù)num返回接收的字節(jié)
//地址結(jié)構(gòu)peer_addr返回發(fā)送數(shù)據(jù)方的協(xié)議地址富弦,addrlen返回存儲在peer_addr中的字節(jié)數(shù)
if (num < 0)
{
/*handle exception*/
}
/*handle data*/
......
}
......
sendto函數(shù)
此函數(shù)用于發(fā)送數(shù)據(jù)沟娱,要指明目的地址
sendto函數(shù)如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *to, int addrlen);
//前三個參數(shù)分別為:調(diào)用socket函數(shù)生成的描述符、指向發(fā)送緩沖區(qū)的指針腕柜、發(fā)送的字節(jié)數(shù)
//flags參數(shù)是傳輸控制標(biāo)志济似,其值通常為0,代表所做的操作與write相同盏缤,還有MSG_DONTROUTE砰蠢,MSG_OOB
//函數(shù)sendto的參數(shù)to的類型是套接字地址結(jié)構(gòu),指明數(shù)據(jù)將發(fā)往的協(xié)議地址唉铜,它的大小由addrlen參數(shù)決定
該函數(shù)調(diào)用成功的返回值為發(fā)送數(shù)據(jù)的長度(以字節(jié)為單位)台舱。如果調(diào)用失敗則返回-1,并置相應(yīng)的errno值
UDP套接字編程實(shí)例
程序?qū)崿F(xiàn)的功能是:
- 客戶根據(jù)用戶提供的IP地址潭流,將用戶從終端輸入的信息發(fā)送給服務(wù)器竞惋,然后等待服務(wù)器的回應(yīng)
- 服務(wù)器接收客戶端發(fā)送的信息,并顯示灰嫉,同時顯示客戶的IP地址拆宛、端口號,并向客戶端發(fā)送信息讼撒。如果服務(wù)器接收的客戶信息為“bye”浑厚,則退出循環(huán),并關(guān)閉套接字椿肩。
- 客戶接收瞻颂、顯示服務(wù)器發(fā)回的信息豺谈,并關(guān)閉套接字
UCP服務(wù)器端程序如下(UDP_server.c):
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 1234 //定義端口號
#define MAXDATASIZE 100 //定義接收緩沖區(qū)大小
main()
{
int sockfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t len;
int num;
char buf[MAXDATASIZE];
/*creating UDP socket*/
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{//調(diào)用socket函數(shù)郑象,產(chǎn)生UDP套接字。如果出錯打印錯誤信息茬末。
perror("Creating socket failed.");
exit(1);
}
bzero(&server, sizeof(server));//初始化server套接字地址結(jié)構(gòu)厂榛,并對地址結(jié)構(gòu)中的成員賦值
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{//將套接字和指定的協(xié)議地址綁定
perror("Bind() error.");
exit(1);
}
len = sizeof(client);
while(1)
{
num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&client, &len);
//接收客戶端的信息,并存放在buf中丽惭,客戶端的地址信息存放在client地址結(jié)構(gòu)中击奶。如果成功num返回接收的字符串的長度。
if (num < 0)
{
perror("recvfrom() error\n");
exit(1);
}
buf[num]='\0';
printf("You got a message <%s> from client.\nIt's ip is %s, port is %d.\n", buf, inet_ntoa(client.sin_addr), htons(client.sin_port));
//顯示接收到的客戶信息责掏、客戶的IP地址和端口號柜砾。通過inet_ntoa()函數(shù)將IP地址轉(zhuǎn)換成可顯示的ASCII串,通過htons()函數(shù)將端口號轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
sendto(sockfd, "Welcome\n", 8, 0, (struct sockaddr *)&client, len);//發(fā)送Welcome字符串給客戶端
if (!strcmp(buf, "bye")) //如果客戶端發(fā)來的字符串是"bye"换衬,則退出循環(huán)
{
break;
}
}
close(sockfd);
}
UCP客戶端程序如下(UDP_client.c):
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT 1234 //這里的端口號要和服務(wù)器的端口號一樣
#define MAXDATASIZE 100
int main(int argc, char const *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in server, peer;
socklen_t len;
if (argc != 3) //檢查用戶的輸入
{
printf("Usage: %s <IP Address><message>\n", argv[0]);
exit(1);
}
if ((he = gethostbyname(argv[1])) == NULL)
{
printf("gethostbyname() error\n");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1){
//調(diào)用socket()函數(shù)產(chǎn)生套接字描述符
printf("socket() error\n");
exit(1);
}
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr); /*he->h_addr the first ip*/
sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&server, sizeof(server));
//將用戶從命令行輸入的消息發(fā)送給服務(wù)器server
len = sizeof(server);
while(1)
{
if ((num = recvfrom(sockfd, buf, MAXDATASIZE, 0, (struct sockaddr *)&peer, &len)) == -1)
//接收服務(wù)器發(fā)過來的字符串痰驱,并保存在buf中证芭。接收的真正字節(jié)數(shù)被存儲在num中,同時peer返回接收服務(wù)器的地址
{
printf("recvfrom() error\n");
exit(1);
}
if (len!= sizeof(server) || memcmp((const void *)&server, (const void *)&peer, len) != 0)
//由于UDP套接字是無連接的担映,它可能接收到其他服務(wù)器發(fā)來的信息废士,所以應(yīng)判斷信息是否來自于相應(yīng)的服務(wù)器。
//首先蝇完,比較recvfrom()函數(shù)調(diào)用后返回的地址長度len是否等于結(jié)構(gòu)體server的長度官硝。如果不是,則說明消息來自于其它服務(wù)器短蜕。
//然后判斷server和peer變量中的內(nèi)容是否一致氢架。如果一致,則說明收到的消息來自于相應(yīng)的服務(wù)器忿危。
//注意达箍,server和peer使用memcmp函數(shù)進(jìn)行比較時,首先應(yīng)轉(zhuǎn)換成常量指針才能使用铺厨。
{
printf("Receive message from other server.\n");
continue;
}
buf[num] = '\0';
printf("Server Message: %s.\n", buf);//顯示來自于服務(wù)器的信息
break;
}
close(sockfd);
}
代碼運(yùn)行截圖
可以多個客戶端同時向服務(wù)器發(fā)送消息
connect函數(shù)用于UDP
UDP調(diào)用connect函數(shù)缎玫,沒有三路握手過程,內(nèi)核只是記錄與之通信的對方的IP地址和端口號解滓,它們包含在傳遞給connect的套接口地址結(jié)構(gòu)中赃磨,并立即返回給調(diào)用進(jìn)程
調(diào)用了connect函數(shù)的UDP套接字為已連接UDP套接字
UDP程序調(diào)用了connect函數(shù),將指定與之通信的對方的IP地址和端口號洼裤,只與唯一對方通信邻辉,只能使用recv或read接收數(shù)據(jù),只能使用send或write發(fā)送數(shù)據(jù)
詳細(xì)介紹:
第5章 并發(fā)服務(wù)器
兩種并發(fā)技術(shù): 多進(jìn)程腮鞍、多線程
可以同時處理多個客戶請求的服務(wù)器稱為并發(fā)服務(wù)器
Linux提供三種方式支持并發(fā): 進(jìn)程
值骇、線程
、I/O多路復(fù)用
TCP并發(fā)服務(wù)器:
多進(jìn)程并發(fā)服務(wù)器
進(jìn)程基礎(chǔ)
進(jìn)程
是執(zhí)行中的計算機(jī)程序移国,是在執(zhí)行過程中不斷變化的動態(tài)的實(shí)體吱瘩。進(jìn)程
是獨(dú)立的,未經(jīng)允許迹缀,一個進(jìn)程不能訪問另一個進(jìn)程的資源使碾,一個進(jìn)程的崩潰不會造成其他進(jìn)程崩潰。
進(jìn)程創(chuàng)建
可以調(diào)用fork
和vfork
函數(shù)來創(chuàng)建新進(jìn)程祝懂。在創(chuàng)建新進(jìn)程時票摇,要進(jìn)行資源拷貝。Linux有三種資源拷貝的方式:
- 共享:新老進(jìn)程共享通用的資源砚蓬,共用一個數(shù)據(jù)結(jié)構(gòu)
- 直接拷貝:將父進(jìn)程的文件矢门、文件系統(tǒng)、虛擬內(nèi)存等結(jié)構(gòu)直接拷貝到子進(jìn)程中。子進(jìn)程創(chuàng)建后祟剔,父子進(jìn)程擁有相同的結(jié)構(gòu)
- Copy on Write:把真正的虛擬內(nèi)存拷貝推遲到兩個進(jìn)程中的任一個試圖寫虛擬頁的時候傅事。如果某虛擬內(nèi)存頁上沒有出現(xiàn)寫的動作,父子進(jìn)程就一直共享該頁而不用拷貝
fork函數(shù)
fork用于普通進(jìn)程的創(chuàng)建峡扩,采用Copy on Write
方式
#include <unistd.h>
pid_t fork(void);
函數(shù)調(diào)用失敗返回-1蹭越,失敗原因:
- 系統(tǒng)中已經(jīng)有太多的進(jìn)程
- 該實(shí)際用戶ID的進(jìn)程總數(shù)超過了系統(tǒng)限制
函數(shù)調(diào)用成功,返回兩次:
- 父進(jìn)程中教届,返回值是新派生的子進(jìn)程的ID號
- 子進(jìn)程中响鹃,返回值為0
為什么在fork的子進(jìn)程中返回的是0,而不是父進(jìn)程id?
原因在于: 所有子進(jìn)程都只有一個父進(jìn)程案训,它可以通過調(diào)用getppid函數(shù)來得到父進(jìn)程的ID买置,而對于父進(jìn)程,它有很多個子進(jìn)程强霎,它沒有辦法通過一個函數(shù)得到各子進(jìn)程的ID忿项。如果父進(jìn)程想跟蹤所有子進(jìn)程的ID,它必須記住fork的返回值
fork函數(shù)的用法如下:
......
pid_t pid;
if((pif = fork()) > 0)
{
//parent process
}
else if(pid == 0)
{
//child process
exit(0); //子進(jìn)程必須用exit函數(shù)退出
}
else
{
printf("fork() error\n");
exit(0);
}
......
vfork函數(shù)
vfork采用共享
的方式創(chuàng)建城舞,新老進(jìn)程共享同樣的資源轩触,完全沒有拷貝
當(dāng)使用vfork()創(chuàng)建新進(jìn)程時,父進(jìn)程將被暫時阻塞家夺,而子進(jìn)程則可以借用父進(jìn)程的地址空間運(yùn)行
脱柱。這個奇特狀態(tài)將持續(xù)直到子進(jìn)程要么退出,要么調(diào)用execve()拉馋,至此父進(jìn)程才繼續(xù)執(zhí)行榨为。
#include <unistd.h>
pid_t vfork(void);
函數(shù)調(diào)用失敗返回-1
函數(shù)調(diào)用成功,返回兩次:
- 父進(jìn)程中煌茴,返回值是新派生的子進(jìn)程的ID號
- 子進(jìn)程中随闺,返回值為0
通過下面的程序來比較fork和vfork的不同
使用vfork函數(shù)
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
int status;
if ((pid = vfork()) == 0) //產(chǎn)生子進(jìn)程
{
sleep(2);
printf("child running.\n");
printf("child sleeping.\n");
sleep(5);
printf("child dead.\n");
exit(0);
}
else if (pid > 0)
{
printf("parent running.\n");
printf("parent exit.\n");
exit(0);
}
else
{
printf("fork error.\n");
exit(0);
}
}
運(yùn)行結(jié)果:
使用fork函數(shù)
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
int status;
if ((pid = fork()) == 0) //產(chǎn)生子進(jìn)程
{
sleep(2);
printf("child running.\n");
printf("child sleeping.\n");
sleep(5);
printf("child dead.\n");
exit(0);
}
else if (pid > 0)
{
printf("parent running.\n");
printf("parent exit.\n");
exit(0);
}
else
{
printf("fork error.\n");
exit(0);
}
}
運(yùn)行結(jié)果:
進(jìn)程終止
進(jìn)程終止存在兩種可能: 父進(jìn)程先于子進(jìn)程終止
、子進(jìn)程先于父進(jìn)程終止
如果父進(jìn)程在子進(jìn)程之前終止蔓腐,則所有子進(jìn)程的父進(jìn)程被改為init進(jìn)程矩乐,就是由init進(jìn)程領(lǐng)養(yǎng)
。在一個進(jìn)程終止時合住,系統(tǒng)會逐個檢查所有活動進(jìn)程绰精,判斷這些進(jìn)程是否是正要終止的進(jìn)程的子進(jìn)程撒璧。如果是透葛,則該進(jìn)程的父進(jìn)程ID就更改為1(init的ID)。這就保證了每個進(jìn)程都有一個父進(jìn)程
父進(jìn)程可以通過調(diào)用wait()或waitpid()函數(shù)卿樱,獲得子進(jìn)程的終止信息僚害。
wait函數(shù)
#include <sys/wait.h>
pid_t wait(int *statloc);
wait函數(shù)的用法如下:
pid_t pid;
if((pid = fork()) > 0)
{
...... //parent process
int chdstatus;
wait(&chdstatus);
}
else if(pid == 0)
{
...... //child process
exit(0);
}
else
{
printf("fork() error\n");
exit(0);
}
waitpid函數(shù)
此函數(shù)對等待哪個進(jìn)程終止及是否采用阻塞操作方式方面給了更多的控制
waitpid函數(shù)如下:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int option);
當(dāng)參數(shù)pid=-1,option=0時,該函數(shù)等同于wait()函數(shù)
參數(shù)pid指定了父進(jìn)程要求知道哪些子進(jìn)程的狀態(tài):
- pid = -1: 要求知道任何一個子進(jìn)程的終止?fàn)顟B(tài)
- pid = 0: 要求知道進(jìn)程號為pid的子進(jìn)程的終止?fàn)顟B(tài)
- pid取值小于-1時萨蚕,要求知道進(jìn)程組號為pid的絕對值的子進(jìn)程的終止?fàn)顟B(tài)
參數(shù)option讓用戶指定附加選項靶草。最常用的選項是WHO_HANG,它通知內(nèi)核在沒有已終止子進(jìn)程時不要阻塞
當(dāng)前有終止的子進(jìn)程時岳遥,返回值為子進(jìn)程的ID號奕翔,同時參數(shù)statloc返回子進(jìn)程的終止?fàn)顟B(tài),否則返回值為-1
waitpid函數(shù)的用法如下:
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WHOHANG)) > 0)
printf("child %d terminated\n", pid);
exit函數(shù)
本函數(shù)用來終止進(jìn)程浩蓉,返回狀態(tài)
#include <stdlib.h>
void exit(int status);
本函數(shù)終止調(diào)用進(jìn)程派继,關(guān)閉所有子進(jìn)程打開的描述符,向父進(jìn)程發(fā)送SIGCHLD信號捻艳,并返回狀態(tài)驾窟,隨后父進(jìn)程就可通過調(diào)用wait或waitpid函數(shù)獲得終止子進(jìn)程的狀態(tài)了
多進(jìn)程并發(fā)服務(wù)器
多進(jìn)程并發(fā)服務(wù)器建立過程
建立連接->父進(jìn)程調(diào)用fork()函數(shù)產(chǎn)生子進(jìn)程->父進(jìn)程關(guān)閉已連接套接字;子進(jìn)程關(guān)閉監(jiān)聽套接字->子進(jìn)程處理客戶請求认轨,父進(jìn)程等待另一個客戶連接
下面用圖例說明父進(jìn)程調(diào)用fork生成子進(jìn)程后绅络,父、子進(jìn)程對描述符的操作過程
當(dāng)服務(wù)器調(diào)用accept函數(shù)時嘁字,連接請求從客戶到達(dá)服務(wù)器時雙方的狀態(tài)如下圖所示:
客戶的連接請求被服務(wù)器接收后恩急,新的已連接套接字即connfd被創(chuàng)建,可通過此描述符讀纪蜒、寫數(shù)據(jù)假栓,此時狀態(tài)如下圖所示:
服務(wù)器的下一步就是調(diào)用fork函數(shù),如下圖所示霍掺,給出了從fork函數(shù)返回后的狀態(tài)匾荆,此時描述符listenfd和connfd在父、子進(jìn)程間共享:
接下來就由父進(jìn)程關(guān)閉已連接描述符(connfd)杆烁,由子進(jìn)程關(guān)閉監(jiān)聽描述符(listenfd)牙丽,當(dāng)前雙方狀態(tài)如下圖所示:
到此就是套接字的最終狀態(tài)
。子進(jìn)程處理與客戶的連接兔魂,父進(jìn)程可以對監(jiān)聽描述符再次調(diào)用accept烤芦,繼續(xù)處理下一個客戶的連接請求