目錄
- 一、網路發展和分層
- 1、網路的體系結構
- 2、網路的拆包和分包
- 二、網路預備基礎知識
- 1、socket
- 2、IP 地址
- 3、埠號
- 4、位元組序
- 三、系統呼叫分析
- 1、分析 server 部分函式
- (1)socket 函式的作用:
- (2)bind 函式的作用:
- (3)listen 函式:
- (4)accept 函式(重要)
- (5)撰寫程式:
- 2、分析 client 部分
- (1)connect 函式
- (2)撰寫程式
- (3)程式優化
- 四、TCP并發服務器
- 1、多執行緒
- 2、多行程
- 3、多路復用實作
大綱分類:

一、網路發展和分層
Internet 的歷史:
1、最早是 ARPAnet (阿帕網)(1974 年)
-
它可以使 不同作業系統 和 不同型別的計算機之間進行資料傳輸,
-
但是沒有糾錯功能, (TCP 協議擁有糾錯能力)
2、TCP/IP 協議
- TCP :專門用來檢測 網路傳輸中 差錯的傳輸控制協議,(沒有絲毫差錯)
- IP :針對于 不同網路進行互聯 的互聯網協議 IP,
1、網路的體系結構
分層的思想:
1、每一層實作不同的功能,對上層的資料做透明傳輸,
2、每次層向上層提供服務,同時又使用下層的服務,
一些術語:
兩層交換機:OSI 的資料鏈路層的交換機,(偏硬體)
三層交換機:OSI 的網路層的交換機,(偏軟體)

- 思考:為什么路由器只到了 IP 層?(分層,主機層面的 端到端)

在分析之前,我們一定要反問自己兩個問題:
1、這一層對上一層提供了哪些功能?
2、這一層實作了哪些功,解決了什么問題?
網路介面和物理層:
- Linux驅動:每個硬體都對應有一個驅動,通過這個驅動完成對這個硬體的操作,
- 這一層有:wifi、GPRS、3G、4G
- 主要作用:屏蔽硬體的差異,不管是什么硬體,都會給網路層提供一個統一的介面,
- net_device 結構是二層中一個非常重要的結構,其結構中成員很多,包含了硬體資訊,介面資訊,其他輔助資訊,以及設備操作函式等等
網路層:(IP 層)
- 主要功能:實作 端到端傳輸的功能,(將資料傳輸到某臺主機上面)
- 使用下層哪些功能:對于網路層來說,不管底層是什么的網路設備,操控都一樣,
傳輸層:
-
情景:Linux 是一個多任務運行的機器,一臺手機發資料到 Linux 主機上面,那么這段資料到底應該發送到 Linux 的哪一個行程上呢?
-
作用:決定了資料包應該交給哪一個行程進行處理,
-
使用了網路層的什么功能:有了網路層,才能將資料傳輸范圍確定在某臺主機上面,
分析每一層的典型協議:
網路介面與物理層:
MAC 地址:48位的一個數字,網路設備的身份標識,
物理層只認識 MAC 地址,并不認識什么 IP 地址,什么埠號,
- ARP:地址決議協議,( IP 轉換為 MAC)
- RARP:逆向地址決議協議,(MAC 轉化為 IP)
- PPP協議:撥號協議(GPRS/3G/4G)
網路層:
- IP協議:Internet protocol :Internet 協議:實作端到端(主機層面)的傳輸,盡力傳輸,
- ICMP:Internet control manage :控制管理協議:ping 命令使用該協議
- IGMP:Internet group manage :分組管理協議:廣播、組播
傳輸層:
- TCP:Transfer Control 傳輸控制:提供可靠連接,出錯重新傳輸,
- UDP:user Datagram 用戶資料報:提供盡力傳輸,(行程層面)
應用層:
- HTTP/HTTPS :網頁訪問協議,
- FTP:檔案輸出協議
- Telnet/SSH:遠程登陸傳輸,
- RTP/RTSP :用于傳輸音視頻協議,安防行業,(基于 TCP+UDP 來實作的)
2、網路的拆包和分包
情景:理解通過 FTP 協議,進行檔案傳輸的時候,通過了哪些步驟?

首先:如何從應用層進入到內核:
通過系統呼叫:即 socket 的相關介面,

上圖我們要掌握兩點:
MTU:Max Transfer Unit ,最大的傳輸單元,
- 和網路的型別有關!!!,不同的網路型別不同,
- 以太網:1500位元組, 802.3 :1492 位元組,
封包:
-
上層添加:TCP頭、IP 頭,
-
物理層加的幀頭:根據硬體的不同添加的不同:以太網頭、WiFi 頭等等,
-
驅動的作用:將封裝好的資料包,送到對應的硬體上面,
-
硬體層:會自動在資料包的尾部,添加 CRC 校驗(32位、4個位元組)
拆包:
- 硬體層:拿到資料包之后,將以太網幀頭拆掉,交給網路層的時候最前面就是 IP 頭了,
二、網路預備基礎知識
1、socket

什么是 socket?
- 是一種編程的介面,是一種系統呼叫,是一種特殊的檔案描述符,是一種 IO 操作,
- 是一種行程間通信,不同兩臺主機上的兩個行程通信,(定位到行程,所以在傳輸層)
socket 的分類:
1、SOCK_STREAM:TCP/IP ,面向連接型,(傳輸層)
2、SOCK_DGRAM:UDP,無連接服務,(傳輸層)
3、SOCK_RAW:IP、ICMP,直接與網路層進行通信,跨過了傳輸層,(網路層)
- ping 命令就是直接利用 socket,直接跳過了傳輸層,直接和網路層互動,
2、IP 地址
1、表現形式
- 日常表示:點分形式,192.168.1.141(字串)
- 網路網路傳輸:32位整數,
2、IP 地址的分類
| 名稱 | 地址 |
|---|---|
| 局域網IP | 192.XXX.XXX.XXX 10.XXX.XXX.XXX |
| 廣播 IP | xxx.xxx.xxx.255 255.255.255.255(全網廣播) |
| 組播IP |
3、埠號
(1)作用:區分一臺主機接受到資料包之后,應該交給哪一個任務來進行處理,(任務包含:行程、執行緒)
(2)是一個 16 位的數字:1-65535
(3)TCP 埠號與 UDP 埠號相互獨立,
(4)埠的分類
- 縱所周知埠:1-1023 (FTP 21、SSH 22、HTTP 80、HTTPS 469)
- 保留埠:1024-5000 (不建議使用)
- 可以使用埠,
可以看出 UDP 、TCP 的資料被分為了兩路,

4、位元組序
x86、arm 采用小端模式,(怎么記憶小端模式:權重小的放到低地址)
powerpc / mips:arm作為路由器、使用大端模式,
網路傳輸的時候采用大端模式,
問題:資料經過多個路由器轉化資料,最終位元組序是什么,到底被處理成了什么是一個問題,
引入:本地位元組序、網路位元組序,
本地位元組序 ——> 網路位元組序,
網路位元組序 ——> 本地位元組序,
位元組序轉換函式:
// 主機位元組序轉化為網路位元組序
nl: network long
ns: network short
u_long htonl(u_long hostlog); // 4 位元組轉換
u_long htons(u_long hostlog); // 2 位元組轉化
// 網路位元組序到主機位元組序
u_long ntohl(u_long hostlog);
u_long ntohs(u_long hostlog);
IP 地址的轉換:(字串 ——> int 型別)
inet_aton:
inet_addr:
- 內部已經包含了位元組序的轉換,所以 ip 地址就不需要我們再次進行轉化了,
- 僅僅適用于 ipv4 的地址轉換,
- 當出錯的時候回傳 -1 ,這樣會產生一個局限性:
- 計算機當中存放 -1, 以補碼的方式來進行存放,255 即 -1,
- 所以,此函式不能用于 255.255.255.255 的轉換,(因為分不清楚到底是出錯,還是成功)
inet_ntoa:
inet_pton:
- 適用于 ipv4、ipv6 的轉換
- 能正確處理 255.255.255.255 的轉化問題
- 引數: af:地址協議族、src:點分形式的IP地址 ,dst:轉化的結果
- 回傳值:1 成功,0 有問題,
三、系統呼叫分析
1、socket 原來都是主動的,但是 經過 bind 、listen 的教育,就變成了被動連接,(其實是 listen 一個函式教育的)
2、主動的 socket 可以主動建立連接,被動的 socket 只能等待被連接,
3、accept 函式回傳 ns, (回傳一個新的檔案描述符,new fs),拿到新的 fd ,那么資料通信就具體到了 兩個行程,
4、三次握手發生在:connect 、accept 兩個函式之間,(listen 當中的 backlog 與三次握手有關),
5、connect 系結的是 服務器的 ip 和 埠號,那么自己客戶端的 ip 和埠號什么時候發送過去呢?
- 在內核層面,傳輸層和網路層會自動為我們進行添加,

1、分析 server 部分函式
(1)socket 函式的作用:
1、確定是 IPV4、還是 IPV6 的 IP地址,
2、確定是 TCP、還是 UDP 的通訊
3、回傳一個檔案描述符,
int socket(int domain, int type, int protocol);
// 引數分析:
1、domain
AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
2、type
SOCK_STREAM :流式套接字,唯一對應 TCP(傳輸層)
SOCK_DGRAM : 資料報套接字,唯一對應 UDP(傳輸層)
SOCK_RAW : 原始套接字,對應直接訪問網路層 IP、ICMP,例如 ping命令
3、protocol
(1)因為資料報套接字和流式套接字都唯一對應著 TCP 與 UDP,所以使用這兩種type 的時候,一般填入0.
(2)原始套接字,按照需要進行填充,
// 回傳值分析
成功的時候回傳檔案描述符,失敗的時候回傳 -1,
(2)bind 函式的作用:
1、確定 ip 地址(32位整數,不是點分形式)
2、確定 埠號
然后將:IP地址和埠號,與這個 socket 回傳的檔案描述符進行系結, (我們操作這個檔案描述符,就是對應某個IP地址和埠)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 引數分析:
1、sockfd:sockfd 回傳的檔案描述符
2、const struct sockaddr *addr :結構體變數的首地址
3、socklen_t addrlen:結構體變數的長度
如何填充結構體?
1、先填充 struct sockaddr_in 這個結構體,然后將他強制轉換為: struct sockaddr 結構體,
- sin_family :網路域
- sin_port:網路位元組序埠號,(使用 htons 來進行轉換,沒有點分形式的轉換,)
- sin_addr:網路位元組序的 ip 地址,(可以使用 ip 地址的專用轉換函式: 點分形式 ——> int、本機位元組序 ——> 網路位元組序 )

struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
(3)listen 函式:
作用:將主動套接字,變為被動套接字
int listen(int sockfd, int backlog);
// 引數分析:
sockfd :socket 回傳的檔案描述符
backlog :指同時允許幾路客戶端正在和服務器進行連接,(也就是正在三次握手程序),一般填入 5,ARM 最大為 8
// 回傳值
成功:回傳 0,失敗回傳 -1
內核服務器當中的套接字 fd 會維護兩個鏈表:
1、正在三次握手的客戶端鏈表,(數量 = 2*backlog +1 )
2、已經建立好連接的客戶端鏈表,(已經完成 3 次握手分配好了 newfd)
(4)accept 函式(重要)
作用:
1、阻塞等待客戶端請求(阻塞函式:一般情況都是在阻塞,等待客戶端連接, 只有當客戶端連接之后,才能不阻塞,)
2、得到一個新的 fd,new fd, (舊的 fd ,可以回傳去繼續等待連接)
3、得到客戶端的 IP 地址、和埠號
4、
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 引數分析
sockfd:經過前面 socket() 創建,bind(),listen() 修改過的 fd,
// 回傳值:
struct sockaddr *addr:拿到客戶端的 ip 地址、埠號
socklen_t *addrlen:
// 可以看出,connect 這一端發送的是輸入型引數
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
回傳值:
成功的時候:回傳建立好連接的,新的 fd ,
失敗的時候:回傳 -1,
(5)撰寫程式:
頭檔案
#ifndef __NET_H_
#define __NET_H_
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <error.h>
#include <stdio.h>
#define Server_Port 5001
#define Server_IP "172.25.58.104"
#define QUIT ".quit"
#endif
server 端程式
#include "net.h"
int main(int argc, const char * argv[])
{
int fd = -1;
struct sockaddr_in sin;
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 第二步:bind sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
// 位元組序、點分形式的轉換
if(inet_pton(AF_INET,Server_IP,(void *)&sin.sin_addr.s_addr) <= 0)
{
printf("inet_pton error");
}
if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
}
// 第三步:listen 將socket 設定為被動的socket
if(listen(fd,5) < 0)
{
perror("listen");
}
// 第四步:accept 阻塞等待連接
int newfd = -1;
if((newfd = accept(fd,NULL,NULL)) < 0)
{
perror("accept");
}
// 第五步:連接上之后,處理新的 fd
char buf[256] = {0};
int ret = -1;
while(1)
{
do
{
ret = read(newfd,buf,sizeof(buf));
}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
if(ret == 0) // 對方關閉了 socket
{
printf("資料接收完畢\n");
break ; // 跳出 while 1 回圈
}
printf("receive : %s \n",buf);
if( !strncasecmp(buf,QUIT,strlen(QUIT)))
{
break;
}
}
close(newfd);
close(fd);
}
注意:
1、ip 地址和 埠號必須轉化為網路位元組序的格式,
2、分析 client 部分
(1)connect 函式
- 作用:連接服務器,也是阻塞函式,(當連接上服務器之后,就不阻塞,函式繼續執行)
- 系結的是 server 的地址 + 埠號,
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
這個函式和我們的 bind 函式相當類似,
(2)撰寫程式
client 端程式
#include "net.h"
int main(int argc,const char *argv[])
{
int fd = -1;
struct sockaddr_in sin;
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 第二步:connect sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
// 位元組序、點分形式的轉換
if(inet_pton(AF_INET,Server_IP,(void *)&sin.sin_addr.s_addr) <= 0)
{
printf("inet_pton error");
}
if( connect(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("connect");
}
// 開始發送資料
char buf[256] = {0};
while(1)
{
scanf("%s",buf);
write(fd,buf,strlen(buf));
if( !strncasecmp(buf, QUIT,strlen(QUIT)) ) // 用戶輸入了quit ,strncasecmp 表示不區別大小寫
{
printf("Client is exiting!\n");
break; //跳出回圈寫
}
}
close(fd);
}
自己寫的 垃圾 makefile
all:server client
server:server.o net.h
gcc server.o net.h -o server
client:client.o net.h
gcc client.o net.h -o client
server.o:server.c
gcc -c server.c -o server.o
client.o:client.c
gcc -c client.c -o client.o
clean:
rm -rf client server client.o server.o
(3)程式優化
之前 sever 端的問題:
1、bind 系結的 ip 地址是固定的,程式每換一臺電腦,就需要更改一下 ip 地址,
解決辦法:使用 INADDR_ANY 宏,
首先我們分析一下 bind 函式的作用:
- 通過系結 ip 地址,來系結是哪一個網卡發來的資料,
- 通過系結 埠號,來系結是哪一個任務接收資料,
所以:INADDY_ANY 宏的作用:系結本機的所有網卡(不管是虛擬的,還是物理的),只要是在當前 Linux 系統,就與此 socket 進行系結,
sin.sin_addr.s_addr = htonl(INADDR_ANY);
2、服務器端不知道客戶端的ip地址,因為我們 accept 當中直接填的NULL,
修改:
// 函式原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(clinet_in);
int nfd = -1;
// 在 clinet_in 當中,就可以獲取到 client 的ip地址,但是是int型別的
if(nfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen) < 0); // 接收客戶端的IP地址和埠號
{
perror("accept");
}
// 將int型別的地址,轉化為 點分形式的
// 函式源型 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
char ipv4_addr[16]; // 定義一個陣列接收 ip 地址的字串
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
printf("Clinet( %s:%d ) is connected!",ipv4_addr, ntohs(clinet_in.sin_port));
3、bind:Address already in use
問題分析:地址不能快速重用,必須等 2 分鐘才能繼續
解決問題:在 bind 之前加一段代碼:
int b_reuse = 1;
setsockopt(fd, SOL,SOCKET, SO_REUSEADDR, &b_reuse,sizof(int));
4、修改以后的 服務端
#include "dxdnet.h"
int main(int argc, const char * argv[])
{
int fd = -1;
struct sockaddr_in sin;
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 優化3:允許地址快速重用
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
// 第二步:bind sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
}
// 第三步:listen 將socket 設定為被動的socket
if(listen(fd,5) < 0)
{
perror("listen");
}
// 第四步:accept 阻塞等待連接
int newfd = -1;
struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(struct sockaddr_in);
newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號
if(newfd < 0)
{
printf("%d \n",newfd);
perror("accept");
return -1;
}
char ipv4_addr[16]; // 定義一個陣列接收 ip 地址的字串
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
// 第五步:連接上之后,處理新的 fd
char buf[256] = {0};
int ret = -1;
while(1)
{
do
{
ret = read(newfd,buf,sizeof(buf));
}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
if(ret == 0) // 對方關閉了 socket
{
printf("資料接收完畢\n");
break ; // 跳出 while 1 回圈
}
printf("receive : %s \n",buf);
if( !strncasecmp(buf,QUIT,strlen(QUIT)))
{
break;
}
}
close(newfd);
close(fd);
}
四、TCP并發服務器
之前 sever 端的問題:
1、accept 會進行阻塞、read 會進行阻塞,所以有多個客戶端連接,那么就沒有辦法進行解決,
解決辦法:多路復用、多執行緒,

1、多執行緒
(0)編譯檔案:
config.mk 檔案
CFLAGS = -c -g -Wall -I include
CC := gcc
LDFLAGS = -lpthread
makefile檔案
include config.mk
.PHONY:all clean
all:server client
server:server.o
gcc server.o -o server -lpthread
client:client.o
server.o:server.c
client.o:client.c
clean:
rm -rf *.o server client
(1)頭檔案
#ifndef __DXDNET_H_
#define __DXDNET_H_
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <error.h>
#include <stdio.h>
#include <pthread.h>
#define Server_Port 5001
#define Server_IP "172.25.58.104"
#define QUIT ".quit"
typedef struct Client_Info
{
int c_fd;
char c_ipv4_addr[16];
int c_port;
}C_Info;
#endif
(2)server 端
- 主執行緒:死回圈、阻塞 accept ,來一個客戶端就開一個執行緒,
- 子執行緒:處理客戶端的程式,
缺點:
- 沒有執行緒回識訓制,創建行程之后,資源無法回收,
- 服務器端結束,客戶端還不知道,
- 客戶端斷開連接,服務端也還不知道
#include "dxdnet.h"
void *clinet_hander(void *argc);
int main(int argc, const char * argv[])
{
int fd = -1;
int ret = -1;
struct sockaddr_in sin;
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 優化3:允許地址快速重用
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
// 第二步:bind sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
}
// 第三步:listen 將socket 設定為被動的socket
if(listen(fd,5) < 0)
{
perror("listen");
}
// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
int newfd = -1;
struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(struct sockaddr_in);
char ipv4_addr[16]; // 定義一個陣列接收 ip 地址的字串
pthread_t pth = -1;
C_Info c1; // 客戶端結構體,傳給子執行緒
while(1)
{
newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號
if(newfd < 0)
{
printf("%d \n",newfd);
perror("accept");
return -1;
}
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
// 給子執行緒傳遞的變數賦值
c1.c_fd = newfd;
strcpy(c1.c_ipv4_addr,ipv4_addr);
c1.c_port = ntohs(clinet_in.sin_port);
// 增加一個執行緒來處理客戶端的資訊:
ret = pthread_create(&pth,NULL,clinet_hander,&c1);
if(0 != ret)
{
perror("pthread_create");
}
}
close(newfd);
close(fd);
}
void *clinet_hander(void *argc)
{
// 第五步:連接上之后,處理新的 fd
char buf[256] = {0};
int ret = -1;
C_Info c1ient = *(C_Info *)argc;
while(1)
{
do
{
ret = read(c1ient.c_fd,buf,sizeof(buf));
}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
if(ret == 0) // 對方關閉了 socket
{
printf("資料接收完畢\n");
break ; // 跳出 while 1 回圈
}
printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
if( !strncasecmp(buf,QUIT,strlen(QUIT)))
{
break;
}
}
close(c1ient.c_fd);
return NULL;
}
(3)client 端:
#include "dxdnet.h"
void *clinet_hander(void *argc);
int main(int argc, const char * argv[])
{
int fd = -1;
int ret = -1;
struct sockaddr_in sin;
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 第二步:bind sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
}
// 第三步:listen 將socket 設定為被動的socket
if(listen(fd,5) < 0)
{
perror("listen");
}
// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
int newfd = -1;
struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(struct sockaddr_in);
char ipv4_addr[16]; // 定義一個陣列接收 ip 地址的字串
pthread_t pth = -1;
C_Info c1; // 客戶端結構體,傳給子執行緒
while(1)
{
newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號
if(newfd < 0)
{
printf("%d \n",newfd);
perror("accept");
return -1;
}
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
// 給子執行緒傳遞的變數賦值
c1.c_fd = newfd;
strcpy(c1.c_ipv4_addr,ipv4_addr);
c1.c_port = ntohs(clinet_in.sin_port);
// 增加一個執行緒來處理客戶端的資訊:
ret = pthread_create(&pth,NULL,clinet_hander,&c1);
if(0 != ret)
{
perror("pthread_create");
}
}
close(newfd);
close(fd);
}
void *clinet_hander(void *argc)
{
// 第五步:連接上之后,處理新的 fd
char buf[256] = {0};
int ret = -1;
C_Info c1ient = *(C_Info *)argc;
while(1)
{
do
{
ret = read(c1ient.c_fd,buf,sizeof(buf));
}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
if(ret == 0) // 對方關閉了 socket
{
printf("資料接收完畢\n");
break ; // 跳出 while 1 回圈
}
printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
if( !strncasecmp(buf,QUIT,strlen(QUIT)))
{
break;
}
}
close(c1ient.c_fd);
return NULL;
}
2、多行程
問題:
- 行程間通信怎么解決?
- 子行程退出之后,變成僵尸行程怎么辦?
- 使用什么函式,對子行程進行回收呢?
分析:
1、fork() 的時候,子行程會繼承 父行程的一些東西, (fork 之前定義的變數,父行程和子行程都有)
2、子行程退出的時候,會給父行程發送信號,( SIGCHLD 信號)
typedef void (*sighandler_t)(int); // 規定了處理函式的源型
sighandler_t signal(int signum, sighandler_t handler);
signal(SIGCHLD,sig_clild_handle); // 系結信號的對應處理函式
3、使用 waitpid 函式來進行回收,可以放在系結的信號處理程式當中進行,
void sig_clild_handle(int signo)
{
if(SIGCHLD == signo)
{
waitpid(-1,NULL, WNOHANG); // WNOHANG :函式不阻塞
}
}
修改完畢:
注意:主行程執行完 else,就會繼續向下執行,重新被 accept 阻塞,
#include "dxdnet.h"
void *clinet_hander(void *argc);
void sig_child_handle(int signo)
{
if(SIGCHLD == signo)
{
waitpid(SIGCHLD,NULL,WNOHANG);
}
}
int main(int argc, const char * argv[])
{
int fd = -1;
int ret = -1;
struct sockaddr_in sin;
signal(SIGCHLD, sig_child_handle); // 系結信號處理函式
// 第一步:創建 socket
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
}
// 優化3:允許地址快速重用
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse,sizeof(int));
// 第二步:bind sever 的ip 和 埠號
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(Server_Port);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(fd,(const struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
}
// 第三步:listen 將socket 設定為被動的socket
if(listen(fd,5) < 0)
{
perror("listen");
}
// 第四步:accept 阻塞等待連接,并接收 客戶端的ip地址和埠號
int newfd = -1;
struct sockaddr_in clinet_in;
socklen_t addrlen = sizeof(struct sockaddr_in);
char ipv4_addr[16]; // 定義一個陣列接收 ip 地址的字串
pid_t pid; // 執行緒 id
C_Info c1; // 客戶端結構體,傳給子執行緒
while(1)
{
newfd = accept(fd, (struct sockaddr *)&clinet_in, &addrlen); // 接收客戶端的IP地址和埠號
if(newfd < 0)
{
printf("%d \n",newfd);
perror("accept");
return -1;
}
// 開啟一個子行程
pid = fork();
if(pid < 0)
{
perror("fork");
}
if(pid == 0)
{
close(fd); // 關閉舊的檔案描述符
inet_ntop(AF_INET, (const void *)&clinet_in.sin_addr.s_addr, ipv4_addr, sizeof(clinet_in));
printf("Clinet( %s:%d ) is connected! \n",ipv4_addr, ntohs(clinet_in.sin_port));
// 給子行程傳遞的變數賦值
c1.c_fd = newfd;
strcpy(c1.c_ipv4_addr,ipv4_addr);
c1.c_port = ntohs(clinet_in.sin_port);
clinet_hander(&c1);
return 0; // 子執行緒退出,并且給主執行緒發送信號
}
else
{
close(newfd); // 關閉新檔案描述符
// 這里什么也不用管了,如果出現信號,會到相應的中斷處理函式當中執行,
// sleep(10);
}
}
close(newfd);
close(fd);
}
void *clinet_hander(void *argc)
{
// 第五步:連接上之后,處理新的 fd
char buf[256] = {0};
int ret = -1;
C_Info c1ient = *(C_Info *)argc;
while(1)
{
do
{
ret = read(c1ient.c_fd,buf,sizeof(buf));
}while(ret < 0); // 如果沒有讀取到,并且沒有中斷產生就一直阻塞
if(ret == 0) // 對方關閉了 socket
{
printf("資料接收完畢\n");
break ; // 跳出 while 1 回圈
}
printf("receive[ %s : %d ] : %s \n", c1ient.c_ipv4_addr, c1ient.c_port ,buf);
if( !strncasecmp(buf,QUIT,strlen(QUIT)))
{
break;
}
}
close(c1ient.c_fd);
return NULL;
}
3、多路復用實作
貼一個github 上找的原始碼吧,懶得寫了,
https://github.com/yuanrw/tcp-server-client,
服務端注意的點:
當服務端 socket 被 listen 之后,有客戶端進行 connect 的話,服務端的 old fd 會回應,因此 select 會停止阻塞,從而去 accept 產生新的 newfd,再次被添加到 fd_set 當中,重新進行 select ,
if(FD_ISSET(serverfd, &client_fdset))
{
struct sockaddr_in client_addr; //新的客戶端連接
size_t size = sizeof(struct sockaddr_in);
int sock_client = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
if(sock_client<0)
{
perror("accept error!\n");
continue;
}
if(conn_amount<5)
{
client_sockfd[conn_amount++] = sock_client;
bzero(buffer, 1024);
strcpy(buffer, "this is a server! welcome!\n");
send(sock_client, buffer, 1024, 0); //把內容傳給新來的客戶端
printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
bzero(buffer, sizeof(buffer));
ret = recv(sock_client, buffer, 1024, 0);
if(ret<0)
{
perror("recv error!\n");
close(serverfd);
return -1;
}
printf("recv : %s\n", buffer);
if(maxsock<sock_client)
maxsock = sock_client;
else
{
printf("max connections!!!quit!!\n");
break;
}
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/303849.html
標籤:其他
