網(wǎng)絡(luò)編程學(xué)習(xí)筆記---Linux+C語言(未完待續(xù))

第一章 TCP/IP簡介

基本的C/S服務(wù)模型

網(wǎng)絡(luò)編程是指編寫的網(wǎng)絡(luò)通信程序可以與網(wǎng)絡(luò)上的其他程序進行通信。

TCP/IP四層結(jié)構(gòu)

網(wǎng)絡(luò)接口層

網(wǎng)際層負責(zé)相鄰互聯(lián)網(wǎng)上的不同主機之間的通信,主要包括IPv4、ICMP、RIP、IGMP

傳輸層負責(zé)主機中兩個進程之間的通信,主要包括TCP、UDP

應(yīng)用層直接為用戶的應(yīng)用進程提供服務(wù)

套接字編程就是應(yīng)用層到傳輸層的接口(API)

TCP協(xié)議介紹

TCP(傳輸控制協(xié)議)是TCP/IP體系中面向連接的運輸層協(xié)議,它可以保證數(shù)據(jù)可靠的傳輸。

碼元比特(6位):分為6個標志,置1表示有效

URG和緊急指針配合使用,發(fā)送緊急數(shù)據(jù)

ACK指出確認字段是否有效

PSH接收方應(yīng)該盡快將這個報文提交給應(yīng)用層

RST重建連接

SYN同步序號用來請求建立連接

FIN用來釋放連接

TCP連接建立與終止

TCP是面向連接的協(xié)議。TCP連接的建立和釋放是每一次通信中必不可少的過程。
TCP連接的建立需要經(jīng)過三次數(shù)據(jù)傳輸。也就是三次握手。

建立TCP連接
  1. 服務(wù)器準備好接收客戶的連接請求 -> socket、bind、listen函數(shù)
  2. 客戶主動打開 -> connect函數(shù),SYN分節(jié)(用來請求建立連接)
  3. 服務(wù)器收到客戶端發(fā)來的SYN分節(jié)后,必須發(fā)送ACK對其確認,同時發(fā)送SYN分節(jié)給客戶端,表示接受客戶端建立連接的請求 -> SYN分節(jié),ACK(指出確認字段是否有效)
  4. 客戶端發(fā)送ACK確認服務(wù)器的SYN -> ACK(指出確認字段是否有效)
  5. 連接建立成功
釋放TCP連接
  1. 客戶端主動關(guān)閉連接 -> close函數(shù),F(xiàn)IN分節(jié)
  2. 服務(wù)器收到FIN分節(jié)后執(zhí)行被動關(guān)閉,并關(guān)閉套接字 -> 發(fā)送ACK(對客戶端的FIN分進行確認),close函數(shù),F(xiàn)IN分節(jié)
  3. 客戶端接受到FIN分節(jié)后,發(fā)送ACK確認分節(jié)后,徹底關(guān)閉連接

TCP連接中的分組交換

TCP連接中的分組交換

UDP協(xié)議介紹

UDP(用戶數(shù)據(jù)報協(xié)議)是面向無連接的服務(wù),提供不可靠的數(shù)據(jù)傳輸。

第二章 套接字編程簡介

套接字基礎(chǔ)

套接字是一種網(wǎng)絡(luò)API(應(yīng)用程序編程接口),可以用它來開發(fā)網(wǎng)絡(luò)程序

套接字接口提供一種進程間通信的方法,使得在相同或不同的主機上的進程能以相同的規(guī)范進行雙向信息傳送

套接字接口是應(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ù)的準確到達。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位是流量標號,下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)

套接字地址結(jié)構(gòu)作為參數(shù)傳遞給任一個套接字函數(shù)時,通常通過指針來傳遞

當套接字函數(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é)存儲在起始地址
小段字節(jié)序和大端字節(jié)序

將某給定主機所使用的字節(jié)序稱為主機字節(jié)序。為了使采用不同字節(jié)序的主機能夠互相通信,TCP/IP協(xié)議規(guī)定了網(wǎng)絡(luò)字節(jié)序。

所有主機或路由器在發(fā)送IP數(shù)據(jù)包之前要首先將相應(yīng)的信息轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序。相應(yīng)的,在接收數(shù)據(jù)包后,要將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換成主機字節(jié)序。

主機字節(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代表主機host,n代表網(wǎng)network,s代表短整型short,l代表長整型long

  • htons:將16位的短整型數(shù),從主機字節(jié)序轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
  • htonl:將32位的長整型數(shù),從主機字節(jié)序轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序
  • ntohs:將16位的短整型數(shù),從網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換成主機字節(jié)序
  • ntohl:將32位的長整型數(shù),從網(wǎng)路字節(jié)序轉(zhuǎn)換成主機字節(jié)序

字節(jié)操縱函數(shù)

以字母b(byte)打頭

#include <string.h>
void bzero(void *dest, size_t len); 
//bzero函數(shù)將目標中指定數(shù)目的字節(jié)置為0,經(jīng)常用此函數(shù)來對套接字地址結(jié)構(gòu)進行初始化
void bcopy(const void *src, void *dest, size_t len);
 //bcopy函數(shù)將指定數(shù)目的字節(jié)從源拷貝到目標
int bcmp(const void *src, void *dest, size_t len);
 //bcmp函數(shù)比較源和目標兩個字符串,若相同返回值為0,否則返回非0值

以mem(memory)打頭

void *memset(void *dest, int x, size_t len); 
//將目標指定數(shù)目的字節(jié)置為值x
void *memcpy(void *dest, const void *src, size_t len);
//將指定數(shù)目的字節(jié)從源拷貝到目標
int memcmp(const void *str1, const void *str2, size_t len); 
//比較兩個字符串,若相同返回值為0,否則返回非0值。如果str1所指字節(jié)大于str2所指字節(jié),則返回值大于0,否則返回值小于0

IP地址轉(zhuǎn)換函數(shù)

用于字符串的IP和二進制值的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)中的二進制值。

inet_addr函數(shù)

將字符串形式的IP地址轉(zhuǎn)換成32位二進制值的IP地址。str指向字符串形式的IP地址。函數(shù)調(diào)用成功,返回值為32位二進制值的IP地址。

inet_aton函數(shù)

將字符串形式的IP地址轉(zhuǎn)換成32位二進制值的IP地址。str指向字符串形式的IP地址。numstr指向轉(zhuǎn)換后的32位網(wǎng)絡(luò)字節(jié)序的IP地址。如果成功返回1,否則返回0

inet_ntoa函數(shù)

將一個32位網(wǎng)絡(luò)字節(jié)序的二進制值的IP地址轉(zhuǎn)換成相應(yīng)的點分十進制的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代表地址的表達格式是ASCII串。n代表數(shù)值格式,是存在于套接字地址結(jié)構(gòu)中的二進制值。

這兩個函數(shù)中的family參數(shù),指的是操作地址的地址族,IPv4是AF_INET,IPv6是AF_INET6

inet_pton函數(shù)

將指針str所指的字符串形式的IP地址,轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序的二進制值的IP地址,并用指針numstr存儲。如果成功返回1,如果對于指定的family輸入的字符串不是一個有效的表達格式,則返回值為0,出錯返回-1

inet_ntop函數(shù)

將numstr所指的二進制值的IP地址轉(zhuǎn)換成字符串形式的IP地址,并用指針str存儲。參數(shù)len是目標的大小,為了避免函數(shù)溢出其調(diào)用者的緩沖區(qū)。

五個地址轉(zhuǎn)換函數(shù)

五個地址轉(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ù)

當把套接字地址結(jié)構(gòu)傳遞給套接字函數(shù)時,總是通過指針來傳遞的,即傳遞的是一個指向套接字地址結(jié)構(gòu)的指針,結(jié)構(gòu)的長度也可用參數(shù)傳遞。

從進程到內(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)核,所以進程到內(nèi)核拷貝的數(shù)據(jù)長度是確定的


從內(nèi)核到進程傳遞套接字地址結(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)時不會越界。而當函數(shù)返回時,結(jié)構(gòu)大小的值發(fā)生了變化,變成了內(nèi)核在此結(jié)構(gòu)中確切存儲的數(shù)據(jù)長度。進程可以通過這個值得到內(nèi)核寫入多少信息到這個結(jié)構(gòu)中。這種在參數(shù)傳遞的過程中,其值發(fā)生變化了的參數(shù)稱為值-結(jié)果參數(shù),內(nèi)核和進程間的兩種傳遞參數(shù)的方式如下圖:

內(nèi)核和進程間傳遞參數(shù)

第三章 基本TCP套接字編程

3.1 TCP套接字編程

使用TCP套接字編程可以實現(xiàn)基于TCP/IP協(xié)議的面向連接的通信,它分為服務(wù)器和客戶端兩部分:

TCP客戶/服務(wù)器的套接字函數(shù)

TCP套接字編程中,服務(wù)器端實現(xiàn)的步驟:

  1. 使用socket()函數(shù)創(chuàng)建套接字
  2. 使用bind函數(shù)為創(chuàng)建的套接字綁定到指定的地址結(jié)構(gòu)
  3. listen()函數(shù)設(shè)置套接字為監(jiān)聽模式,使服務(wù)器進入被動打開的狀態(tài)
  4. 接受客戶端的連接請求,建立連接
  5. 接收、應(yīng)答客戶端的數(shù)據(jù)請求
  6. 終止連接

客戶端實現(xiàn)的步驟:

  1. 使用socket()函數(shù)創(chuàng)建套接字
  2. 調(diào)用connect()函數(shù)建立一個與TCP服務(wù)器的連接
  3. 發(fā)送數(shù)據(jù)請求,接收服務(wù)器的數(shù)據(jù)應(yīng)答
  4. 終止連接

socket函數(shù)

為了執(zhí)行網(wǎng)絡(luò)I/O,無論是服務(wù)器還是客戶端,首先必須調(diào)用socket函數(shù),產(chǎn)生TCP套接字,作為TCP通信的傳輸端點

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

socket函數(shù)中family參數(shù)指明協(xié)議族。type參數(shù)指明產(chǎn)生套接字的類型。protocol參數(shù)是協(xié)議標志,一般在調(diào)用socket函數(shù)時將其置為0,但如果是原始套接字,就需要為protocol指定一個常值。

該函數(shù)調(diào)用成功,返回一個小的非負的整數(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表示組合無效:

sock函數(shù)中協(xié)議族和套接字類型的組合

調(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的三次握手過程。建立與遠程服務(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),在進行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ù)器主機在指定的端口上沒有進程在等待與之相連??蛻舳笋R上返回錯誤ECONNREFUSED
  • 如果客戶發(fā)出的SYN在中間路由器上引發(fā)一個目的地不可達ICMP錯誤,客戶端內(nèi)核保存此消息,并按第一種情況,連續(xù)重傳SYN,直到規(guī)定時間的超時時間,對方仍沒有響應(yīng),則返回保存的消息(即ICMP錯誤)EHOSTUNREACH或ENETUNREACH錯誤返回給進程

對于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)強制轉(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的端口號

對于綁定操作,地址信息必須是唯一的,在實際應(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地址中的任意一個,可以兩個都指定,也可以一個也不指定。如果不綁定任何端口,當調(diào)用connect或listen時,內(nèi)核會為套接字選擇一個臨時的端口。

進程如果綁定了一個特定的本地IP地址到它的套接字上,對于TCP客戶端,這就為在此套接字上發(fā)送的IP數(shù)據(jù)報分配了源IP地址。對于TCP服務(wù)器端,這就限制了該套接字只接收目的地址為此IP地址的客戶連接

函數(shù)綁定的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ù)

當調(diào)用函數(shù)socket創(chuàng)建一個套接字時,默認情況下它是一個主動套接字,也就是一個將調(diào)用connect函數(shù)發(fā)起連接的客戶端套接字。所以對于TCP服務(wù)器,在綁定操作后,必須要調(diào)用listen函數(shù),將這個未連接的套接字轉(zhuǎn)換成被動套接字,監(jiā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ù)目進行了限制,如果一個服務(wù)請求到來時,輸入隊列已滿,該套接字將拒絕連接請求。

函數(shù)調(diào)用成功返回0,出錯返回-1,并置errno值

TCP三次握手中兩個隊列的位置
文字補充

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 //當listen函數(shù)調(diào)用失敗時的異常處理
    ......
}
......

accept函數(shù)

accept函數(shù)使服務(wù)器接受客戶端的連接請求

它將完成隊列中的隊頭條目返回給進程,并產(chǎn)生一個新的套接字描述符——已連接套接字

當已完成隊列為空,則進程睡眠,直到有已完成連接到達時。

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指針所指的值置為client所指的套接字地址結(jié)構(gòu)的長度。函數(shù)返回時,此整數(shù)值變?yōu)閮?nèi)核寫入此套接字地址結(jié)構(gòu)的準確字節(jié)數(shù)。

函數(shù)調(diào)用成功時,可以得到三個值:

  • 一個是accept函數(shù)的返回值,已連接套接字描述符。已連接套接字描述符是內(nèi)核為每個被接受的客戶都分別創(chuàng)建一個。用完則關(guān)閉。監(jiān)聽描述符負責(zé)接收客戶的連接請求,而已連接描述符負責(zé)與對應(yīng)的客戶進行數(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當前的長度
......
connfd = accept(listenfd, (struct sockaddr *)&client, &addrlen);
//調(diào)用accept函數(shù),接收連接請求,返回已連接套接字描述符
//與服務(wù)器連接的客戶端的協(xié)議地址可以通過參數(shù)client得到,addrlen返回內(nèi)核寫入client結(jié)構(gòu)體中的準確字節(jié)數(shù)
if(connfd == -1){
    //handle exception
    ......
}
......

數(shù)據(jù)傳輸函數(shù)

服務(wù)端和客戶端連接建立成功后,就可以進行雙向的數(shù)據(jù)傳輸。服務(wù)端和客戶端使用各自的套接字描述符進行讀寫操作

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是傳輸控制標志,其值定義如下圖:


send函數(shù)的參數(shù)flags

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);
recv函數(shù)的參數(shù)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套接字編程實例

程序?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)默認是只允許一個套接字綁定一個特定的協(xié)議地址上,并且當該套接字關(guān)閉后,系統(tǒng)仍不允許在該地址上綁定其他套接字。
    //如果去掉這兩行,程序運行時產(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當前的長度
    if ((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
    //接受客戶端連接,將客戶的地址信息存放在client地址結(jié)構(gòu)中,&addrlen為內(nèi)核寫入client結(jié)構(gòu)體中的準確字節(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);
    }

    //通過用戶輸入的點分十進制形式的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標志字符串的結(jié)束
    printf("server message: %s\n", buf);//顯示從服務(wù)器接收到的buf中的信息
    close(sockfd); //關(guān)閉套接字
}

代碼運行截圖:

運行截圖

第四章 基本UDP套接字編程

UDP套接字編程

UDP客戶/服務(wù)器的套接字函數(shù)

UDP套接字編程中,服務(wù)器端實現(xiàn)的步驟:

  • 使用socket()函數(shù)創(chuàng)建套接字
  • 為創(chuàng)建的套接字綁定到指定的地址結(jié)構(gòu)
  • 等待接受客戶端的數(shù)據(jù)請求
  • 處理客戶端的請求
  • 向客戶端發(fā)送應(yīng)答數(shù)據(jù)
  • 關(guān)閉套接字

客戶端實現(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ù)是傳輸控制標志,其值通常為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ù)是傳輸控制標志,其值通常為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套接字編程實例

程序?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ù)進行比較時,首先應(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);
}

代碼運行截圖

可以多個客戶端同時向服務(wù)器發(fā)送消息


UDP代碼運行截圖

connect函數(shù)用于UDP

UDP調(diào)用connect函數(shù),沒有三路握手過程,內(nèi)核只是記錄與之通信的對方的IP地址和端口號,它們包含在傳遞給connect的套接口地址結(jié)構(gòu)中,并立即返回給調(diào)用進程

調(diào)用了connect函數(shù)的UDP套接字為已連接UDP套接字

UDP程序調(diào)用了connect函數(shù),將指定與之通信的對方的IP地址和端口號,只與唯一對方通信,只能使用recv或read接收數(shù)據(jù),只能使用send或write發(fā)送數(shù)據(jù)

詳細介紹:


詳細介紹

第5章 并發(fā)服務(wù)器

兩種并發(fā)技術(shù): 多進程多線程

可以同時處理多個客戶請求的服務(wù)器稱為并發(fā)服務(wù)器

Linux提供三種方式支持并發(fā): 進程、線程I/O多路復(fù)用

TCP并發(fā)服務(wù)器:

TCP并發(fā)服務(wù)器

多進程并發(fā)服務(wù)器

進程基礎(chǔ)

進程 是執(zhí)行中的計算機程序,是在執(zhí)行過程中不斷變化的動態(tài)的實體。進程是獨立的,未經(jīng)允許,一個進程不能訪問另一個進程的資源,一個進程的崩潰不會造成其他進程崩潰。

進程創(chuàng)建

可以調(diào)用forkvfork函數(shù)來創(chuàng)建新進程。在創(chuàng)建新進程時,要進行資源拷貝。Linux有三種資源拷貝的方式:

  • 共享:新老進程共享通用的資源,共用一個數(shù)據(jù)結(jié)構(gòu)
  • 直接拷貝:將父進程的文件、文件系統(tǒng)、虛擬內(nèi)存等結(jié)構(gòu)直接拷貝到子進程中。子進程創(chuàng)建后,父子進程擁有相同的結(jié)構(gòu)
  • Copy on Write:把真正的虛擬內(nèi)存拷貝推遲到兩個進程中的任一個試圖寫虛擬頁的時候。如果某虛擬內(nèi)存頁上沒有出現(xiàn)寫的動作,父子進程就一直共享該頁而不用拷貝

fork函數(shù)

fork用于普通進程的創(chuàng)建,采用Copy on Write方式

#include <unistd.h>
pid_t fork(void);

函數(shù)調(diào)用失敗返回-1,失敗原因:

  • 系統(tǒng)中已經(jīng)有太多的進程
  • 該實際用戶ID的進程總數(shù)超過了系統(tǒng)限制

函數(shù)調(diào)用成功,返回兩次:

  1. 父進程中,返回值是新派生的子進程的ID號
  2. 子進程中,返回值為0

為什么在fork的子進程中返回的是0,而不是父進程id?

原因在于: 所有子進程都只有一個父進程,它可以通過調(diào)用getppid函數(shù)來得到父進程的ID,而對于父進程,它有很多個子進程,它沒有辦法通過一個函數(shù)得到各子進程的ID。如果父進程想跟蹤所有子進程的ID,它必須記住fork的返回值

fork函數(shù)的用法如下:

......
pid_t pid;
if((pif = fork()) > 0)
{
     //parent process
}
else if(pid == 0)
{
    //child process
    exit(0); //子進程必須用exit函數(shù)退出
}
else
{
    printf("fork() error\n");
    exit(0);
}
......

vfork函數(shù)

vfork采用共享的方式創(chuàng)建,新老進程共享同樣的資源,完全沒有拷貝

當使用vfork()創(chuàng)建新進程時,父進程將被暫時阻塞,而子進程則可以借用父進程的地址空間運行。這個奇特狀態(tài)將持續(xù)直到子進程要么退出,要么調(diào)用execve(),至此父進程才繼續(xù)執(zhí)行。

#include <unistd.h>
pid_t vfork(void);

函數(shù)調(diào)用失敗返回-1

函數(shù)調(diào)用成功,返回兩次:

  1. 父進程中,返回值是新派生的子進程的ID號
  2. 子進程中,返回值為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)生子進程
    {
        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);
    }
}

運行結(jié)果:

vfork函數(shù)

使用fork函數(shù)

#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    pid_t pid;
    int status;
    if ((pid = fork()) == 0) //產(chǎ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);
    }
}

運行結(jié)果:

fork函數(shù)

進程終止

進程終止存在兩種可能: 父進程先于子進程終止、子進程先于父進程終止

如果父進程在子進程之前終止,則所有子進程的父進程被改為init進程,就是由init進程領(lǐng)養(yǎng)。在一個進程終止時,系統(tǒng)會逐個檢查所有活動進程,判斷這些進程是否是正要終止的進程的子進程。如果是,則該進程的父進程ID就更改為1(init的ID)。這就保證了每個進程都有一個父進程

補充知識

父進程可以通過調(diào)用wait()或waitpid()函數(shù),獲得子進程的終止信息。

wait函數(shù)

#include <sys/wait.h>
pid_t wait(int *statloc);
wait函數(shù)

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ù)對等待哪個進程終止及是否采用阻塞操作方式方面給了更多的控制

waitpid函數(shù)如下:

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int option);

當參數(shù)pid=-1,option=0時,該函數(shù)等同于wait()函數(shù)

參數(shù)pid指定了父進程要求知道哪些子進程的狀態(tài):

  • pid = -1: 要求知道任何一個子進程的終止狀態(tài)
  • pid = 0: 要求知道進程號為pid的子進程的終止狀態(tài)
  • pid取值小于-1時,要求知道進程組號為pid的絕對值的子進程的終止狀態(tài)

參數(shù)option讓用戶指定附加選項。最常用的選項是WHO_HANG,它通知內(nèi)核在沒有已終止子進程時不要阻塞

當前有終止的子進程時,返回值為子進程的ID號,同時參數(shù)statloc返回子進程的終止狀態(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ù)用來終止進程,返回狀態(tài)

#include <stdlib.h>
void exit(int status);

本函數(shù)終止調(diào)用進程,關(guān)閉所有子進程打開的描述符,向父進程發(fā)送SIGCHLD信號,并返回狀態(tài),隨后父進程就可通過調(diào)用wait或waitpid函數(shù)獲得終止子進程的狀態(tài)了

多進程并發(fā)服務(wù)器

多進程并發(fā)服務(wù)器建立過程

建立連接->父進程調(diào)用fork()函數(shù)產(chǎn)生子進程->父進程關(guān)閉已連接套接字;子進程關(guān)閉監(jiān)聽套接字->子進程處理客戶請求,父進程等待另一個客戶連接

下面用圖例說明父進程調(diào)用fork生成子進程后,父、子進程對描述符的操作過程

當服務(wù)器調(diào)用accept函數(shù)時,連接請求從客戶到達服務(wù)器時雙方的狀態(tài)如下圖所示:

客戶的連接請求被服務(wù)器接收后,新的已連接套接字即connfd被創(chuàng)建,可通過此描述符讀、寫數(shù)據(jù),此時狀態(tài)如下圖所示:

服務(wù)器的下一步就是調(diào)用fork函數(shù),如下圖所示,給出了從fork函數(shù)返回后的狀態(tài),此時描述符listenfd和connfd在父、子進程間共享:

接下來就由父進程關(guān)閉已連接描述符(connfd),由子進程關(guān)閉監(jiān)聽描述符(listenfd),當前雙方狀態(tài)如下圖所示:

到此就是套接字的最終狀態(tài)。子進程處理與客戶的連接,父進程可以對監(jiān)聽描述符再次調(diào)用accept,繼續(xù)處理下一個客戶的連接請求

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容