目录
- 一、网络发展和分层
- 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;
}
}
}