当前位置:首页 » 《休闲阅读》 » 正文

网络编程基础(TCP)_vincent3678的博客

12 人参与  2022年02月01日 15:24  分类 : 《休闲阅读》  评论

点击全文阅读


目录

  • 一、网络发展和分层
      • 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、多路复用实现

大纲分类:

image-20210822171007473

一、网络发展和分层

Internet 的历史:

1、最早是 ARPAnet (阿帕网)(1974 年)

  • 它可以使 不同操作系统不同类型的计算机之间进行数据传输。

  • 但是没有纠错功能。 (TCP 协议拥有纠错能力)

2、TCP/IP 协议

  • TCP :专门用来检测 网络传输中 差错的传输控制协议。(没有丝毫差错)
  • IP :针对于 不同网络进行互联 的互联网协议 IP。

1、网络的体系结构

分层的思想:

1、每一层实现不同的功能,对上层的数据做透明传输。

2、每次层向上层提供服务,同时又使用下层的服务。

一些术语:

两层交换机:OSI 的数据链路层的交换机。(偏硬件)

三层交换机:OSI 的网络层的交换机。(偏软件)

image-20210925213311581

  • 思考:为什么路由器只到了 IP 层?(分层,主机层面的 端到端)

image-20210925220355752

在分析之前,我们一定要反问自己两个问题:

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 协议,进行文件传输的时候,通过了哪些步骤?

image-20210925222442937

首先:如何从应用层进入到内核:

通过系统调用:即 socket 的相关接口

image-20210925222501409

上图我们要掌握两点

MTU:Max Transfer Unit ,最大的传输单元

  • 和网络的类型有关!!!,不同的网络类型不同。
  • 以太网:1500字节。 802.3 :1492 字节。

封包:

  • 上层添加:TCP头、IP 头。

  • 物理层加的帧头:根据硬件的不同添加的不同:以太网头、WiFi 头等等。

  • 驱动的作用:将封装好的数据包,送到对应的硬件上面

  • 硬件层:会自动在数据包的尾部,添加 CRC 校验(32位、4个字节)

拆包:

  • 硬件层:拿到数据包之后,将以太网帧头拆掉,交给网络层的时候最前面就是 IP 头了。

二、网络预备基础知识

1、socket

image-20210925224123246

什么是 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 地址的分类

名称地址
局域网IP192.XXX.XXX.XXX 10.XXX.XXX.XXX
广播 IPxxx.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 的数据被分为了两路

image-20210925232159169


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 和端口号什么时候发送过去呢?

  • 在内核层面,传输层和网络层会自动为我们进行添加。

image-20210420114138217


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命令
3protocol    
	(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 返回的文件描述符
2const struct sockaddr *addr :结构体变量的首地址
3socklen_t addrlen:结构体变量的长度

如何填充结构体?

1、先填充 struct sockaddr_in 这个结构体,然后将他强制转换为: struct sockaddr 结构体。

  • sin_family :网络域
  • sin_port:网络字节序端口号。(使用 htons 来进行转换,没有点分形式的转换。)
  • sin_addr:网络字节序的 ip 地址。(可以使用 ip 地址的专用转换函数: 点分形式 ——> int、本机字节序 ——> 网络字节序

image-20210420100538386

           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;
				}
			}
		}

点击全文阅读


本文链接:http://zhangshiyu.com/post/34164.html

地址  函数  字节  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1