网络编程(OSI模型、TCP/IP模型、UDP及TCP通信、并发服务器、I/O多路复用与线程池、组播及广播)笔记-day14

本文深入探讨了网络编程的基础知识,包括TCP/IP四层模型、OSI七层模型、IP地址与字节序转换、UDP通信和TCP通信的工作流程。特别强调了UDP的无连接不可靠特性,以及TCP的面向连接和可靠性。还详细介绍了如何使用select函数实现I/O多路复用,以及线程池的概念。最后,提到了广播和组播的概念及其在UDP中的应用。
摘要由CSDN通过智能技术生成

目录

前言

一、网络体系结构

1.1OSI 七层模型

1.2TCP/IP 四层模型

1.3IP地址(点分制、整型)

1.4字节序转序、端口号

二、UDP通信

2.1UDP简介

2.2接收端

2.3发送端

三、TCP通信

3.1TCP简介

3.2客户端

3.3服务器

3.4 综合试炼

四、循环服务器模型

4.1并发服务模型(多线程)

4.2 select多路I/O复用

4.3线程池

五、单播 组播 广播

5.1 广播(UDP)

5.2 IP地址类别、子网掩码

5.3 组播(UDP)

总结


 


前言

网络编程被分为上下两篇,上篇将整理网络体系结构(OSI 七层模型、TCP/IP 四层模型、IP地址(点分制、整型)及字节序转序、端口号);UDP及TCP通信(发送与接受端、服务器与客户端);循环服务器模型(多线程并发服务器、I/O多路复用与线程池);组播及广播的学习笔记,

下篇则是sqlite数据库相关知识。

千里之行,始于足下!!


提示:以下是本篇文章正文内容,下面案例可供参考

一、网络体系结构

网络体系结构:网络的分层结构及每层使用的协议集合

1.1OSI 七层模型

    (1)OSI(Open System Interconnection)
     七层模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
		7.应用层 //FTP、E-mail
		
		6.表示层 //数据格式定义,数据转换加密		
		5.会话层 //建立通信进程的逻辑名字与物理名字之间的联系
		
		4.传输层 //差错处理、恢复,流量控制,提供可靠的数据传输
		3.网络层 //数据分组,路由选择
		
		2.数据链路层 //数据组成可发送和接收的帧
		1.物理层 //传输物理信号、接口、信号形式、速率

1.2TCP/IP 四层模型

(2)TCP/IP四层模型(应用层、传输层、网络层、网络接口层)
	
	4.应用层
		ftp (File Transfer Protocol)   //文件传输协议
		http(Hyper Text Transfer Protocol) //超文本传输协议 
		DNS (Domain Name Server) //域名解析协议 DNS dns
		DHCP(Dynamic Host Configuration Protocol) //动态获取IP 
		SMTP(Simple Mail Transfer Protocol)//简单邮件传输协议
	3.传输层:实现了应用程序间端对端的通信
		TCP  UDP 
	2.网络层:为数据包选择路由
		IP		ICMP  //ping 命令使用icmp协议 
		
	1.网络接口:将二进制流转换成数据帧,进行发送,数据帧是网络传输的最资本单元
		ARP   通过IP地址得到MAC地址 


网络管理相关命令(重点)//如何查看自己windows电脑的IP地址
		
		ipconfig //查看windows IP地址 
		那么,我们Linux系统里面,如何查看自己的IP地址,命令 ifconfig
		1) ifconfig : 查看或者设置ip地址	ifconfig   查看ip地址	
			ifconfig eth0 192.168.40.252		//设置ip地址
		
		2) ping  查看对方主机是否在线 
			
			TTL 初始化值 64 128
			
			TTL ?  计算通信过程经过的路由的个数, 每经过一个路由ttl减1, 直到减到0, 数据发送失败	
			TTL 值是 52  ----> 中间经过的路由个数 64 - 52 = 12 个路由
				
		3) nc (nc是netcat的简写,有着网络界的瑞士军刀美誉)  模拟 tcp服务器 tcp客户端  udp 服务器、客户端
		
			如果没有安装的话(sudo apt-get install netcat)
			主要功能是网络测试(测试端口使用情况,测试网络)
			启动tcp服务器端, 使用端口号 6666
				nc -l 6666     (6666 端口号)    -l 启动tcp服务器
	
			启动tcp客户端
				nc 127.0.0.1 6666  (要写服务器的ip地址和端口号)
				127.0.0.1  (环回地址  专门用来做网络程序测试用的  自发自收)	
			网络通信(两方  收  发)
				启动udp接收端: nc -ul 9999   (1 - 65535 之间)
				启动udp发送端: nc -u 127.0.0.1 4444

1.3IP地址(点分制、整型)

linux下网络通信又称为socket编程,socket通信
2 IP地址(网络中的地址)

	//dos端查看自己的IP地址 
	
	Windows查看自己的IP地址:windows + r ---> cmd ----> ipconfig 
	Ubuntu中查看IP地址:ifconfig
	ping 192.168.0.5//查看对方主机是否在线
	
	IP地址是主机要与其他机器通信必须具有的一个IP地址 
	 
	1、IP地址的两种表示方式
	1) 点分制 ("192.168.1.21"  字符串) //字符串13个字节
	2) 整型   0xC0A80115
	
	1个字节 == 8bit(8个二进制位)
	1个十六进制位对应4个二进制位 
	0xC0A80115 ---> 32个二进制位 
	32 / 8bit == 4字节 
	
	整型IP地址,用4个字节就可以存储
	//0xC0  192 
	//0xA8  168 
	//0x01  1
	//0x15  21
	
	网络通信中使用整型IP地址
	//两个函数,实现点分形式 和 整型形式 IP地址相互转换 
	
	点分形式的IP地址转为整型
	整型的IP地址转换为点分

	2、 inet_addr函数  //将点分形式的ip地址转换成整型的ip地址
	typedef unsigned int uint32_t;
	typedef uint32_t in_addr_t;
	 in_addr_t inet_addr(const char *cp);
	 
	功能:将点分制的ip地址转成整型ip地址
	const char *cp //保存的是 "192.168.1 21" 字符串的首地址,被转换的点分IP地址
	返回值:返回值就是整型的IP地址

/my.h///
#ifndef _MY_H
#define _MY_H
//如何避免头文件的重复包含

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <netinet/in.h>
#include <sys/socket.h>

#endif

/inet_addr例子案例//
#include "my.h"

int main(int argc, const char *argv[])
{
	//将点分形式的IP地址转为为整型的IP地址
	in_addr_t addr = inet_addr("192.168.1.21");
	printf("整型IP地址是%X\n",addr);//%X以十六进制的格式打印输出
	return 0;
}

//打印输出
整型IP地址是1501A8C0 //发现打印的整型IP地址是反过来的

##结论: inet_addr函数的返回值,就是大端模式的网络字节序的IP地址
	3、 inet_ntoa函数
	char *inet_ntoa(struct in_addr in);
	
	 struct in_addr {
               in_addr_t s_addr; //结构体成员变量就是 整型的IP地址 
           };
	功能:将整型的IP地址转换成点分格式的IP地址)
	返回值:char* 点分形式IP地址字符串的首地址"192.168.1.21"
/inet_ntoa例子/
#include "my.h"
int main(int argc, const char *argv[])
{
	//将点分形式的IP地址转为为整型的IP地址
	in_addr_t addr = inet_addr("192.168.1.21");
	printf("整型IP地址是%X\n",addr);//%X以十六进制的格式打印输出
	//将整型的IP地址转换为点分形式的IP地址
	struct in_addr s; //因为inet_ntoa函数的参数需要的是结构体 struct in_addr类型
	s.s_addr = addr;
	char* p = (char*)inet_ntoa(s);
	printf("点分形式的IP地址是:%s\n",p);
	return 0;
}	

//打印输出:
整型IP地址是1501A8C0
点分形式的IP地址是:192.168.1.21

1.4字节序转序、端口号

    4.字节序转序
	
    字节序:大端模式、小端模式
	大端模式:高位字节存储在低位地址中,低位字节存储在高位地址中(低对高,高对低)
	小端模式:高位字节存储在高位地址中,低位字节存储在低位地址中(低对低,高对高)
	
	      76543210
int a = 0x12345678
//0x12  高位字节
//0x78  低位字节
0xaaaa0000 //低位地址 
0xaaaa0003 //高位地址

#########################测试当前PC字节序 (通常为小端模式)#######################
#include <stdio.h>

int main(int argc, const char *argv[])
{
	int a = 0x12345678;
	char* p =(char*)&a; //p定义为char* ,是因为char* 类型 p+1 地址量增加1,把int类型中四个字节的数据都取出来
	printf("*(p+0) is %x\n",*(p+0));
	printf("*(p+1) is %x\n",*(p+1));
	printf("*(p+2) is %x\n",*(p+2));
	printf("*(p+3) is %x\n",*(p+3));
	return 0;
}
//打印输出结果:
*(p+0) is 78
*(p+1) is 56
*(p+2) is 34
*(p+3) is 12

p+0 //低地址 
78  //低位字节 
低对低,是小端模式 
通常主机字节序采用的是小端模式

(*****)网络字节序和主机字节序不同点,及相关转换函数。	
主机字节序:通常采用的是小端模式,高位字节存储在高位地址中,低位字节存储在低位地址中(低对低,高对高) 
网络字节序:采用的是大端模式,高位字节存储在低位地址中,低位字节存储在高位地址中(低对高,高对低)
n //network 网络 
h //host 主机 
s //short 短整型 
l //long 长整型

//主机字节序转网络字节序  小端 ---> 大端 
htons() //将主机字节序的短整型转换为网络字节序的短整型
htonl() //将主机字节序的长整型转换为网络字节序的长整型

//将网络字节序转成主机字节序 大端 ---> 小端
ntohs() //将网络字节序的短整型转换为主机字节序的短整型
ntohl() //将网络字节序的长整型转换为主机字节序的长整型


######################################ntohl()例子###################################
#include "my.h"

int main(int argc, const char *argv[])
{
	//将点分形式的IP地址转为为整型的IP地址
	in_addr_t addr = inet_addr("192.168.1.21");
	printf("整型IP地址是%X\n",addr);//%X以十六进制的格式打印输出

	//inet_addr函数的返回值,是一个大端模式的网络字节序地址
	printf("网络字节序转为主机字节序:%X\n",ntohl(addr));
	return 0;
}

//打印输出 
整型IP地址是1501A8C0
网络字节序转为主机字节序:C0A80115

二、UDP通信

2.1UDP简介

  UDP通信
	1.关于UDP通信
	(1)UDP是一个面向无连接的不可靠的传输层协议    //无连接   不可靠  在传输的过程当中容易丢失数据,不可靠
	(2)UDP通信协议位于传输层 
	(3)UDP通信的优点和缺点
		UDP缺点:不可靠,容易丢失数据 
		UDP优点:传输速度快 
	(4)应用场合:传输视频和一些较大的文件 
	
	2. UDP通信,接收端和发送端
     分为接收端  和 发送端
     接收端过程(4歩) 
     (1) 创建一个socket数据报套接字
     (2) 绑定自己的IP地址和端口号
     (3) 接收数据 
     (4) 关闭套接字 close(sockfd);
     
     发送端过程(3歩)
     1. 创建一个数据报套接字socket()   SOCK_DGRAM
	   //(发送的前提条件是你已经知道了接收方的电话号码(也就是接收方的IP地址和端口号))
	 2. 发送数据 sendto() 
	 3. 关闭套接字close() 

2.2接收端

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(1)创建socket数据报套接字
socket函数头文件及函数原型
	##功能:创建一个socket套接字
   #include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int socket(int domain, int type, int protocol);
   
   //调用:
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	参数说明:
	int domain   网络的通信协议 AF_INET  IPv4
	int type     套接字的类型   SOCK_DGRAM 数据报套接字
	int protocol 0 代表自动默认匹配相关协议
	返回值:
		成功:非负套接字描述符  ,socket套接字是一个特殊的文件描述符
		失败: -1 
	
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(2)绑定自己的ip地址和端口号(将进程与IP地址和端口号关联)
端口号:是用来识别应用程序的,如果A程序绑定55555端口号,
		那么网络里面来了一包55555的端口号数据,就发给A程序

int bind(int sockfd, struct sockaddr *addr, int addrlen);
#功能:绑定自己的IP地址和端口号
	
	struct sockaddr_in myaddr = { 0 };//用来保存自己的IP地址和端口号 
	
	myaddr.sin_family = AF_INET;//family协议选择IPv4
	myaddr.sin_port = htons(4444);//端口号 
	myaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //"127.0.0.1"IP地址是主机环回地址
	
	myaddr 类型是 struct sockaddr_in 
	&myaddr 类型是 struct sockaddr_in*
	函数的参数需要的是 struct sockaddr * 
	所以函数的参数需要将&myaddr 强制类型转换 (struct sockaddr*)&myaddr
							
	//为什么我们定义的结构体类型和函数参数上的类型不一样
	bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr))

#参数说明:
	int sockfd //上一步,socket函数的返回值 
	struct sockaddr *addr //用来保存自己的IP地址和端口号的结构体的首地址
	int addrlen //IP地址和端口号的长度
#返回值:
	成功: 0 
	失败: -1

//此结构体在网络通信中主要保存 ip地址 和 端口号 
//此结构体赋值很麻烦
struct sockaddr
{
	short sa_family;     //AF_INET (tcp/ip通信)
	char sa_data[14];    //字符数组有14个字节  1-2字节 端口号 3-6 ip地址  7-14  预留
};

因为  sa_data 存储数据不方便,所以,linux又定义了下面这个结构体

struct sockaddr_in
{
	short sin_family; //AF_INET 
	short sin_port; //端口号    
	struct in_addr sin_addr;    //struct in_addr   4  s_addr //IP地址
	unsigned char sin_zero[8];	//预留
};
 
struct in_addr 
{
   in_addr_t s_addr;
};

myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

因为struct scokaddr 赋值麻烦,所以又定义了一个 struct sockaddr_in ,专门用来赋值的,网络通信时,强制
将struct sockaddr_in转换成struct sockaddr
端口号:是用来识别应用程序的,如果A程序绑定55555端口号,那么网络里面来了一包55555的端口号数据,就发给A程序

有些应用的端口号是固定的

ftp    21		File Transfer Protocol 
http   80		HyperText Transfer Protocol)
DNS    53		Domain Name System
DHCP   67	    Dynamic Host Configuration Protocol

我们写的应用程序尽量用 > 1024的端口号, 

127.0.0.1   环回地址(自发自收,用于网络程序测试用的)
 
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(3)接收数据(阻塞的方式接收数据)

 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

 #功能:阻塞 接收数据
	
//调用 
	
	//不想保存对方的IP地址和端口号
	char buf[100] = { 0 };//用来保存接收到的数据
	recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
	
	//想要保存对方的IP地址和端口号
	struct sockaddr_in youaddr = { 0 };//用来保存对方的IP地址和端口号 
	int len = sizeof(youaddr);
	recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&youaddr, &len);
	
 参数说明:
	int sockfd //第一步socket函数的返回值
	void *buf  //接收到的数据存放的位置
	size_t len  //接收数据的最大字节数
	int flags //0 阻塞接收
	
	struct sockaddr *src_addr  //用来保存对方的IP地址和端口号 
	socklen_t *addrlen //对方iP地址和端口号的长度
	
返回值:
	成功: 实际接收字节数 
	失败: -1

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(4)关闭socket套接字
	close(sockfd);
/my.h///
#ifndef _MY_H
#define _MY_H
//如何避免头文件的重复包含

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <netinet/in.h>
#include <sys/socket.h>

#endif

/recv.c 
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };//用来保存接收到的数据
	//1.创建一个数据报套接字,套接字的类型是SOCK_DGRAM
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);//sockfd is 3
	//2.绑定自己的IP地址和端口号
	struct sockaddr_in myaddr;
	myaddr.sin_family = AF_INET;//IPv4
	myaddr.sin_port = htons(4444);//端口号 
	myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");//IP地址
	bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	//3.阻塞接收数据
	while(1)
	{
		//阻塞接收 ,不想知道对方的IP地址和端口号,第5和6个参数直接赋值为NULL
		recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
		printf("recv is %s\n",buf);
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

///nc命令模拟客户端/
nc -u 127.0.0.1 4444

2.3发送端

发送端:
	1. 创建一个数据报套接字socket()   SOCK_DGRAM
	//(发送的前提条件是你已经知道了接收方的电话号码(也就是接收方的IP地址和端口号))
	2. 发送数据 sendto() 
	3. 关闭套接字close() 

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(1)创建一个socket数据包套接字 
	同接收端 
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(2)指定接收方的IP地址和端口号 
struct sockaddr_in toaddr; //用来保存接收方的IP地址和端口号 

//发送方是当前的他,   toaddr里面存的是对方的手机号
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(4444);
toaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(3) 发送数据 
 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
	char buf[100] = "hello";
	//在调用sendto函数的时候,数据发送给谁,取决于 toaddr里面装的是谁的IP地址和端口号
	sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&toaddr, sizeof(toaddr));

#功能:发送数据 
#函数参数说明:
	int sockfd   //socket函数的返回值  
	const void *buf  //发送数据存放的位置 
	size_t len //实际发送的字节数 
	int flags // 通常为0 
	const struct sockaddr *dest_addr  //接收方的IP地址和端口号 
	socklen_t addrlen //IP地址和端口号的长度
#返回值:
	成功:实际发送的字节数 
	失败: -1
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//(4)关闭套接字
close(sockfd)

///recv.c//
#include "my.h"

int main(int argc, const char *argv[])
{
	int ret;
	char buf[100] = { 0 };//用来保存接收到的数据
	//1.创建一个数据报套接字,套接字的类型是SOCK_DGRAM
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);//sockfd is 3
	//2.绑定自己的IP地址和端口号
	struct sockaddr_in myaddr;
	myaddr.sin_family = AF_INET;//IPv4
	myaddr.sin_port = htons(4444);//端口号 
	myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");//IP地址
	
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	//3.阻塞接收数据
	while(1)
	{
		//阻塞接收 ,不想知道对方的IP地址和端口号,第5和6个参数直接赋值为NULL
		recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
		printf("recv is %s\n",buf);
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
} 

send.c//
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };
	struct sockaddr_in toaddr = { 0 };//用保存接收方的IP地址和端口号
	//toaddr里面存的是谁的IP地址和端口号,就是给谁发送数据
	//1.创建一个数据报套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//发送之前,提前知道接收当的IP地址和端口号 
	toaddr.sin_family = AF_INET;
	toaddr.sin_port = htons(4444);
	toaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	//2.发送数据,但是发送数据之前,你要提前知道接收方的IP地址和端口号
	while(1)
	{
		gets(buf);
		sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&toaddr,sizeof(toaddr));
	}
	//3.关闭套接字
	close(sockfd);
	return 0;
}


//修改后的recv.c

#include "my.h"

int main(int argc, const char *argv[])
{
	int ret;
	char buf[100] = { 0 };//用来保存接收到的数据
	//1.创建一个数据报套接字,套接字的类型是SOCK_DGRAM
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);//sockfd is 3
	//2.绑定自己的IP地址和端口号
	struct sockaddr_in myaddr;
	myaddr.sin_family = AF_INET;//IPv4
	myaddr.sin_port = htons(4444);//端口号 
//	myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");//IP地址
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 宏定义 可以自动的去获取当前自己主机的IP地址

	
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	struct sockaddr_in youaddr = { 0 };//用来保存发送方的IP地址和端口号,知道到底是哪个IP地址给我发送的数据
	int len = sizeof(youaddr);
	//3.阻塞接收数据
	while(1)
	{
		//阻塞接收 ,不想知道对方的IP地址和端口号,第5和6个参数直接赋值为NULL
		//情况1: 她, 她的儿子已经八岁了,态度,无视垃圾短息,第5 6个参数直接赋值为空
		//recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
		//printf("recv is %s\n",buf);
		//情况2: 她, 39岁,一次恋爱没谈过,态度,我要把这个人的手机号深深的刻在心里,保存到youaddr结构体变量中
		recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&youaddr, &len);
		printf("from IP:%s Port:%d %s\n",(char*)inet_ntoa(youaddr.sin_addr), ntohs(youaddr.sin_port),buf);
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

//假设张三做为接收端:
张三自己电脑的IP地址是 192.168.110.157
//假设李四作为发送端
李四自己电脑的IP地址是 192.168.110.220


张三,recv.c 运行在张三电脑上, 做为接收方,张三绑定自己的IP地址的端口号 192.168.110.157 6666
//接收方张三,收到李四发送的数据,李四电脑的IP地址和端口号会保存在youaddr结构体变量中
recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&youaddr, &len);
printf("from IP:%s Port:%d %s\n",(char*)inet_ntoa(youaddr.sin_addr), ntohs(youaddr.sin_port),buf);
//from IP: 192.168.110.220 Port: 随机值 hello world! 


李四,send.c 运行在李四电脑上,做为发送方,李四提前已经知道了张三绑定的IP地址和端口号192.168.110.157  6666,
存在toaddr结构体中,sendto发送数据
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&toaddr,sizeof(toaddr));

 练习:两个进程,每个都能接收,发送(upd聊天程序)   类似于消息队列里面的
./chat 1 2  A:发送的类型是1,接收的类型是2
./chat 2 1  B:发送的类型是2,接收的类型是1

#include "my.h"

//将toaddr定义为全局的
struct sockaddr_in toaddr;

//线程,专门用来输入消息并发送
void* send_fun(void* p)
{
	char buf[100] = { 0 };//用来保存输入的数据
	int sockfd = *((int*)p);
	while(1)
	{
		gets(buf);
		sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&toaddr,sizeof(toaddr));
	}
}

int main(int argc, const char *argv[])
{
	if(argc != 4)
	{
		printf("忘记传递参数了!!\n ./chat 192.168.31.157 33333 44444");
		exit(-1);
	}
	int ret;
	pthread_t id;
	char buf[100] = { 0 };//用来保存接收到的数据
	//1.创建一个数据报套接字,套接字的类型是SOCK_DGRAM
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);//sockfd is 3

	//toaddr是main函数的局部变量,send_recv函数无法直接使用,作用域不够
	//发送之前,是提前知道接收方的IP地址和端口号 
	//argv这个名字是main函数的形参,只能在main函数中使用argv这个名字
	toaddr.sin_family = AF_INET;
	toaddr.sin_port = htons(atoi(argv[2]));
	toaddr.sin_addr.s_addr = inet_addr(argv[1]);


	//2.绑定自己的IP地址和端口号
	struct sockaddr_in myaddr;
	myaddr.sin_family = AF_INET;//IPv4
	myaddr.sin_port = htons(atoi(argv[3]));//端口号 
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 宏定义 可以自动的去获取当前自己主机的IP地址

	
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	//创建一个线程,取循环输入数据并发送
	pthread_create(&id, NULL, send_fun, &sockfd);


	struct sockaddr_in youaddr = { 0 };//用来保存发送方的IP地址和端口号,知道到底是哪个IP地址给我发送的数据
	int len = sizeof(youaddr);
	//3.阻塞接收数据
	//主进程就负责一件事 接收数据,并打印
	while(1)
	{
		recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&youaddr, &len);
		printf("from IP:%s Port:%d %s\n",(char*)inet_ntoa(youaddr.sin_addr), ntohs(youaddr.sin_port),buf);
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

三、TCP通信

tcp 通信 
1.什么是TCP
	TCP是一个面向有连接的可靠的传输层协议
	UDP是一个面向无连接的不可靠的传输层协议
2.TCP协议位于哪一层?
	传输层 
	网络发送数据的单位: 数据帧
3.为什么TCP是可靠的? 3次握手 4次挥手  心跳包 重传确认
	udp 分为接收端 和 发送端
	tcp 分为服务器端 和 客户端

3.1TCP简介

搭建TCP服务器端步骤
1.创建一个流式套接字 SOCK_STREAM  socket()
2.绑定自己的IP地址和端口号 bind()
3.设置监听 listen()
4.阻塞等待连接 aceept()
5.接收数据 recv()
6.发送数据 send()
7.关闭套接字 close()
nc 命令来代替客户端

搭建TCP客户端步骤
1.创建一个socket流式套接字 SOCK_STREAM socket()
2.连接服务器端,已经提前知道了服务器的IP地址和端口号 connect()
3.输入数据,循环发送 send()
4.关闭套接字 close()

3.2客户端

搭建TCP客户端步骤
############################(1)创建一个socket流式套接字###########################
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}

############################(2)连接服务器端#########################################
	 #include <sys/types.h>          /* See NOTES */
     #include <sys/socket.h>
	(1)功能:连接到服务器
		//在连接服务器之前,客户端已经知道了服务器的IP地址和端口号
		//所以我们定义一个
		struct sockaddr_in serveraddr = { 0 };
		serveraddr.sin_family = AF_INET;
		serveraddr.sin_port = htons(6666);
		serveraddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	(2)参数说明:
		int connect(int sockfd, const struct sockaddr *addr,
					   socklen_t addrlen);
		//调用:
		//conect函数,通过serveraddr里面保存的IP地址和端口号,去找到服务器,并连接
		connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
					   
		int sockfd  //socket函数的返回值 
		const struct sockaddr *addr //提前知道的服务器的IP地址和端口号
		socklen_t addrlen //IP地址和端口号的长度
	(3)返回值:
		成功:返回 0
		失败: 返回 -1
############################(3)发送send()##########################################
		char buf[100] = "hello";
		send(sockfd, buf, sizeof(buf), 0);
############################(4)接收recv()##########################################
		char buf[100] = { 0 };
		recv(sockfd, buf, sizeof(buf), 0);
############################(5)关闭socket套接字####################################
		close(sockfd);

客户端代码示例:

/客户端 client.c///
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };//用来保存即将发送的数据
	struct sockaddr_in serveraddr = { 0 };//用来保存提前知道的服务器的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.连接服务器,在连接服务器之前,已经提前知道了服务器的IP地址和端口号
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(6666);
	serveraddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	//3.输入数据,循环发送
	while(1)
	{
		gets(buf);
		send(sockfd, buf, sizeof(buf), 0);
	}
	//4.关闭套接字 
	close(sockfd);
	return 0;
}

3.3服务器

搭建服务器端函数原型、参数及返回值阐述


#############################(1)创建一个socket套接字############################### 

	//调用
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//注意点:TCP通信,套接字的类型要选择 流式套接字 SOCK_STREAM
	同UDP通信
	套接字类型:选择SOCK_STREAM

#############################(2)绑定自己的IP地址和端口号############################

	同UDP通信 
	struct sockaddr_in myaddr = { 0 };
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取自己电脑的IP地址
	//myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");

############################(3)设置监听(允许同时连接的最大个数)#####################
	#include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
	//调用
	listen(sockfd, 5);
	(1)功能:允许同时连接的最大个数 
	(2)参数说明:
		int sockfd  //socket函数的返回值
		int backlog //允许同时连接的最大的个数
	(3)返回值:
		成功: 0
		失败: -1
	(4)实例:
	
############################(4)阻塞等待连接########################################
	#include <sys/types.h>          /* See NOTES */
	#include <sys/socket.h>
	int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	//调用 
	//对第2,3个参数的理解,类似于 UDP里面的youaddr变量
	//不想知道连接服务器的那个客户端的IP地址和端口号,直接赋值为NULL
	int newsockfd = accept(sockfd, NULL, NULL);
	//想要知道连接服务器的那个客户端的IP地址和端口号,可以定义一个结构体变量,参数上的地址传递,得到IP地址和端口号
	struct sockaddr_in clientaddr = { 0 };
	int len = sizeof(clientaddr);
	int newsockfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
	(1)功能:阻塞等到客户端连接
		//一但客户端连接成功,就解除阻塞
	(2)参数说明:
		int sockfd  //socket函数的返回值 
		struct sockaddr *addr //保存的是连接的那个客户端的IP地址和端口号 
		socklen_t *addrlen //客户端的IP地址和端口号的长度
	(3)返回值://##非常非常重要
		成功:返回一个非负的整数,是一个新的套接字描述符 newsockfd,	在TCP通信中只要有了newsockfd的值就可以与客户端通信,不需要知道客户端的IP地址和端口号
		失败: -1
	(5)实例 	
		struct sockaddr_in youaddr;
		int len = sizeof(youaddr);
		int newsockfd = accept(sockfd,(strut scokaddr*)&youaddr,&len);	

############################(5)阻塞接收数据#########################################
	 #include <sys/types.h>
     #include <sys/socket.h>
     ssize_t recv(int sockfd, void *buf, size_t len, int flags);
	//调用
	char  buf[100] = { 0 };//接收数据存放的位置
	recv(newsockfd, buf, sizeof(buf), 0);
	(1)功能: 接收数据
	(2)参数说明: 
		int sockfd  //服务器端,在接收数据的时候,用的是新的套接字描述符 accept函数的返回值 newsockfd
		void *buf //接收数据存放的位置
		size_t len //接收数据的最大长度
		int flags //0 阻塞接收
		
	(3)返回值:
		成功: 实际接收到的字节数
		失败: -1 
	
############################(6)发送数据############################################
	   #include <sys/types.h>
       #include <sys/socket.h>
       ssize_t send(int sockfd, const void *buf, size_t len, int flags);
	   //调用 
	   char buf[100] = "hello world!";
	   send(newsockfd, buf, sizeof(buf), 0);
	(1)功能:发送数据 
	(2)参数说明:
		int sockfd //服务器端,在发送数据的时候,用的是新的套接字描述符 accept函数的返回值 newsockfd
		const void *buf //发送数据存放的位置
		size_t len   //实际发送的字节数 
		int flags //通常 0  
############################(7)关闭套接字##########################################
		close(sockfd);
		close(newsockfd);

服务器端代码示例:

/服务器端 server.c//
#include "my.h"

int main(int argc, const char *argv[])
{
	int ret;
	int newsockfd;
	char buf[100] = { 0 };//用来保存接收到的数据
	struct sockaddr_in myaddr = { 0 };//用来保存自己电脑的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//2.绑定自己电脑的IP地址和端口号
	//由于运行程序,偶尔会出现绑定失败,所以我们判断是否绑定成功,对bind函数的返回值做判断
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind OK!!\n");
	//3.设置监听 ,允许同时连接的最大个数是5个
	listen(sockfd, 5);
	//4.阻塞等待连接,一但有新的客户端连接成功,accept函数立刻解除阻塞,返回值是一个新的套接字描述符
	newsockfd = accept(sockfd, NULL, NULL);//第2,3个参数赋值为NULL,服务器不想知道连接自己的那个客户端的IP地址和端口号
	printf("连接成功!,accept解除阻塞,newsockfd is %d\n",newsockfd);
	while(1)
	{
		//5.阻塞接收数据,recv返回值代表实际接收到的字节数
		ret = recv(newsockfd, buf, sizeof(buf), 0);//注意此处用的是newsockfd 
		if(ret > 0)//接收到数据
		{
			printf("recv is %s\n",buf);
		}
		else //客户端断开连接了
		{
			printf("客户端断开连接!!\n");
			close(newsockfd);
			break;
		}
	}
	//6.关闭套接字
	close(sockfd);
	return 0;
}

3.4 综合试炼

//练习
服务器端:实现服务器端循环收数据打印,并将接收到的数据回传给客户端
//serv.c
#include "my.h"

int main(int argc, const char *argv[])
{
	int ret;
	int newsockfd;
	char buf[100] = { 0 };//用来保存接收到的数据
	struct sockaddr_in myaddr = { 0 };//用来保存自己电脑的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//2.绑定自己电脑的IP地址和端口号
	//由于运行程序,偶尔会出现绑定失败,所以我们判断是否绑定成功,对bind函数的返回值做判断
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind OK!!\n");
	//3.设置监听 ,允许同时连接的最大个数是5个
	listen(sockfd, 5);
	//4.阻塞等待连接,一但有新的客户端连接成功,accept函数立刻解除阻塞,返回值是一个新的套接字描述符
	//newsockfd = accept(sockfd, NULL, NULL);//第2,3个参数赋值为NULL,服务器不想知道连接自己的那个客户端的IP地址和端口号
	struct sockaddr_in clientaddr = { 0 };//用来保存连接服务器的那个客户端的IP地址和端口号
	int len = sizeof(clientaddr);
	newsockfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//第2,3个参数不赋值为NULL,服务器想要知道连接自己的那个客户端的IP地址和端口号
	printf("连接成功!,accept解除阻塞,newsockfd is %d\n",newsockfd);
	while(1)
	{
		//5.阻塞接收数据,recv返回值代表实际接收到的字节数
		ret = recv(newsockfd, buf, sizeof(buf), 0);//注意此处用的是newsockfd 
		if(ret > 0)//接收到数据
		{
			printf("from client IP:%s Port:%d %s\n", (char*)inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), buf);
			//收到打印之后,立刻回传给客户端
			send(newsockfd, buf, sizeof(buf), 0);
		}
		else //客户端断开连接了
		{
			printf("客户端断开连接!!\n");
			close(newsockfd);
			break;
		}
	}
	//6.关闭套接字
	close(sockfd);
	return 0;
}
//client.c/
#include "my.h"

//子线程,专门用来负责接收数据
void* recv_fun(void* p)
{
	int sockfd = *((int*)p);
	char buf[100] = { 0 };//用来保存接收到的数据
	while(1)
	{
		recv(sockfd, buf, sizeof(buf), 0);
		printf("from server: %s\n",buf);
	}
}

int main(int argc, const char *argv[])
{
	if(argc != 3)
	{
		printf("忘记传递参数了!! ./client 192.168.110.157 6666\n");
		exit(-1);
	}
	pthread_t id;
	char buf[100] = { 0 };//用来保存即将发送的数据
	struct sockaddr_in serveraddr = { 0 };//用来保存提前知道的服务器的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.连接服务器,在连接服务器之前,已经提前知道了服务器的IP地址和端口号
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[2]));
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	//创建一个子线程专门负责服务器接收过来的数据
	pthread_create(&id, NULL, recv_fun, &sockfd);

	//3.输入数据,循环发送
	//客户端主进程,只负责输入数据,立刻发送
	while(1)
	{
		gets(buf);
		send(sockfd, buf, sizeof(buf), 0);
	}

	//4.关闭套接字 
	close(sockfd);
	return 0;
}

四、循环服务器模型

多线程并发服务器的思想:只要有一个新的客户端连接服务器,那么服务器端就立刻创建一个线程,与这个客户端交互

4.1并发服务模型(多线程)

server.c
/server.c(多线程创建并发服务器)无视那个客户端的ip 端口号
#include "my.h"

//只要有一个客户端连接,就服务器创建一个新的线程,与客户端交互
void* do_client(void* p)
{
	int ret;
	int newsockfd = *((int*)p);
	char buf[100] = { 0 };
	while(1)
	{
		ret = recv(newsockfd, buf, sizeof(buf), 0);//注意此处用的是newsockfd 
		if(ret > 0)//接收到数据
		{
			printf("%s\n", buf);
			//收到打印之后,立刻回传给客户端
			send(newsockfd, buf, sizeof(buf), 0);
		}
		else //客户端断开连接了
		{
			printf("客户端断开连接!!\n");
			close(newsockfd);
			pthread_exit(NULL);//结束线程
		}
	}
}

int main(int argc, const char *argv[])
{
	int ret;
	pthread_t id;
	int newsockfd;
	char buf[100] = { 0 };//用来保存接收到的数据
	struct sockaddr_in myaddr = { 0 };//用来保存自己电脑的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//2.绑定自己电脑的IP地址和端口号
	//由于运行程序,偶尔会出现绑定失败,所以我们判断是否绑定成功,对bind函数的返回值做判断
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind OK!!\n");
	//3.设置监听 ,允许同时连接的最大个数是5个
	listen(sockfd, 5);
	//4.阻塞等待连接,一但有新的客户端连接成功,accept函数立刻解除阻塞,返回值是一个新的套接字描述符
	//newsockfd = accept(sockfd, NULL, NULL);//第2,3个参数赋值为NULL,服务器不想知道连接自己的那个客户端的IP地址和端口号
	struct sockaddr_in clientaddr = { 0 };//用来保存连接服务器的那个客户端的IP地址和端口号
	int len = sizeof(clientaddr);
	while(1)
	{
		//只要有一个新的客户端连接,就创建一个新的线程,与客户端交互
		newsockfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//第2,3个参数不赋值为NULL,服务器想要知道连接自己的那个客户端的IP地址和端口号
		printf("连接成功!,accept解除阻塞,newsockfd is %d\n",newsockfd);
		pthread_create(&id, NULL, do_client, &newsockfd);
	}
	//6.关闭套接字
	close(sockfd);
	return 0;
}

/多线程并发服务器 想知道客户端ip 端口号
#include "my.h"

typedef struct 
{
	int newsockfd;
	struct sockaddr_in clientaddr;
}data_t;

//只要有一个客户端连接,就服务器创建一个新的线程,与客户端交互

void* do_client(void* p)
{
	int ret;
	data_t s = *((data_t*)p);//拷贝出来一份main函数中的s

	char buf[100] = { 0 };
	while(1)
	{
		ret = recv(s.newsockfd, buf, sizeof(buf), 0);//注意此处用的是newsockfd 
		if(ret > 0)//接收到数据
		{
			printf("from IP:%s Port:%d %s\n", (char*)inet_ntoa(s.clientaddr.sin_addr),ntohs(s.clientaddr.sin_port),buf);
			//收到打印之后,立刻回传给客户端
			send(s.newsockfd, buf, sizeof(buf), 0);
		}
		else //客户端断开连接了
		{
			printf("客户端断开连接!!\n");
			close(s.newsockfd);
			pthread_exit(NULL);//结束线程
		}

	}
}

int main(int argc, const char *argv[])
{
	int ret;
	pthread_t id;
	int newsockfd;
	char buf[100] = { 0 };//用来保存接收到的数据
	struct sockaddr_in myaddr = { 0 };//用来保存自己电脑的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//2.绑定自己电脑的IP地址和端口号
	//由于运行程序,偶尔会出现绑定失败,所以我们判断是否绑定成功,对bind函数的返回值做判断
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind OK!!\n");
	//3.设置监听 ,允许同时连接的最大个数是5个
	listen(sockfd, 5);
	//4.阻塞等待连接,一但有新的客户端连接成功,accept函数立刻解除阻塞,返回值是一个新的套接字描述符
	//newsockfd = accept(sockfd, NULL, NULL);//第2,3个参数赋值为NULL,服务器不想知道连接自己的那个客户端的IP地址和端口号
	struct sockaddr_in clientaddr = { 0 };//用来保存连接服务器的那个客户端的IP地址和端口号
	int len = sizeof(clientaddr);
	//你想要用线程的第四个参数,传递 clientaddr变量 和newsockfd变量,所以定义一个结构体变量
	data_t s;//用来线程传递参数
	while(1)
	{
		newsockfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//第2,3个参数不赋值为NULL,服务器想要知道连接自己的那个客户端的IP地址和端口号
		printf("连接成功!,accept解除阻塞,newsockfd is %d\n",newsockfd);
		s.newsockfd = newsockfd;
		s.clientaddr = clientaddr;
		pthread_create(&id, NULL, do_client, &s);
	}
	//6.关闭套接字
	close(sockfd);
	return 0;
}



client.c 客户端
####################################################################################

#include "my.h"
//子线程,专门用来负责接收服务器发送过来的数据
void* recv_fun(void* p)
{
	int sockfd = *((int*)p);
	char buf[100] = { 0 };//用来保存接收到的数据
	while(1)
	{
		recv(sockfd, buf, sizeof(buf), 0);
		printf("from server: %s\n",buf);
	}
}

int main(int argc, const char *argv[])
{
	if(argc != 3)
	{
		printf("忘记传递参数了!! ./client 192.168.110.157 6666\n");
		exit(-1);
	}
	pthread_t id;
	char buf[100] = { 0 };//用来保存即将发送的数据
	struct sockaddr_in serveraddr = { 0 };//用来保存提前知道的服务器的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.连接服务器,在连接服务器之前,已经提前知道了服务器的IP地址和端口号
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[2]));
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	//创建一个子线程专门负责服务器接收过来的数据
	pthread_create(&id, NULL, recv_fun, &sockfd);

	//3.输入数据,循环发送
	//客户端主进程,只负责输入数据,立刻发送
	while(1)
	{
		gets(buf);
		send(sockfd, buf, sizeof(buf), 0);
	}

	//4.关闭套接字 
	close(sockfd);
	return 0;
}

 多进程服务器模型

/多进程服务器模型//
void* do_client(void* p)
{
	while(1)
	{
		recv();//收到客户端请求
		printf();//端茶倒水、洗衣服做饭、带娃遛狗等待
		send();//给客户端回馈或者响应
	}
}
int main()
{
	while(1)
	{
		//多进程并发服务器的思想:只要有一个新的客户端连接服务器,那么服务器端就立刻创建一个
		//进程,与这个客户端交互
		newsockfd = accept(sockfd, NULL, NULL); //父进程,最主要的工作,时刻等待新的客户端连接
		printf("newsockfd is %d\n",newsockfd);
		pid_t ret = fork();//创建进程
		if(ret == -1)//fork failed 
		{
			perror("fork faield");
			exit(-1);
		}
		else if(ret == 0)//说明是子进程
			do_cleint()//干活 
	}
}

///多进程服务器server.c///
#include "my.h"

//只要有一个客户端连接,就服务器创建一个新的线程,与客户端交互
void do_client(int newsockfd, struct sockaddr_in clientaddr )
{
	int ret;

	char buf[100] = { 0 };
	while(1)
	{
		ret = recv(newsockfd, buf, sizeof(buf), 0);//注意此处用的是newsockfd 
		if(ret > 0)//接收到数据
		{
			printf("from IP:%s Port:%d %s\n", (char*)inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port),buf);
			//收到打印之后,立刻回传给客户端
			send(newsockfd, buf, sizeof(buf), 0);
		}
		else //客户端断开连接了
		{
			printf("客户端断开连接!!\n");
			close(newsockfd);
			exit(0);//结束与客户端交互的子进程
		}
	}
}

int main(int argc, const char *argv[])
{
	int ret;
	pthread_t id;
	int newsockfd;
	char buf[100] = { 0 };//用来保存接收到的数据
	struct sockaddr_in myaddr = { 0 };//用来保存自己电脑的IP地址和端口号
	//1.创建一个流式套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//2.绑定自己电脑的IP地址和端口号
	//由于运行程序,偶尔会出现绑定失败,所以我们判断是否绑定成功,对bind函数的返回值做判断
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(6666);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//myaddr.sin_addr.s_addr = inet_addr("192.168.110.157");
	ret = bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr));
	if(ret == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind OK!!\n");
	//3.设置监听 ,允许同时连接的最大个数是5个
	listen(sockfd, 5);
	//4.阻塞等待连接,一但有新的客户端连接成功,accept函数立刻解除阻塞,返回值是一个新的套接字描述符
	//newsockfd = accept(sockfd, NULL, NULL);//第2,3个参数赋值为NULL,服务器不想知道连接自己的那个客户端的IP地址和端口号
	struct sockaddr_in clientaddr = { 0 };//用来保存连接服务器的那个客户端的IP地址和端口号
	int len = sizeof(clientaddr);
	while(1)
	{
		newsockfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//第2,3个参数不赋值为NULL,服务器想要知道连接自己的那个客户端的IP地址和端口号
		printf("连接成功!,accept解除阻塞,newsockfd is %d\n",newsockfd);
		pid_t ret = fork();
		if(ret == -1)
		{
			perror("fork failed");
			exit(-1);
		}
		else if(ret == 0)//说明是子进程
		{
			do_client(newsockfd, clientaddr);
		}
	}
	//6.关闭套接字
	close(sockfd);
	return 0;
}

4.2 select多路I/O复用

select多路I/O复用原理:

如果tcp服务器用并发服务器(多线程来实现),比较耗费资源;I/O多路复用可以解决这个问题

I/O多路复用原理:
	select函数是一个总的阻塞函数,把所有读阻塞相关的函数recv accept合并在一起,
	归为一个阻塞,当select函数解除阻塞的时候,我们需要对select解除阻塞的原因进行
	分析判断,如果是 新的客户端连接,导致的解除阻塞,调用accept函数
			  如果是 已经连接的客户端,发送消息,导致的解除阻塞,调用recv函数,接收数据
					  
	如何判断到底是哪个原因导致的解除阻塞
	global_rdfs 总的集合,所有读相关的文件描述符都在这个集合中
	current_rdfs 当前的集合,所有读相关的文件描述符都在这个集合中
			
	select(&current_rdfs)函数解除阻塞之后,current_rdfs 集合中,只剩下一个文件描述符,
	就是导致select解除阻塞的那个描述符,判断该描述符的值,来决定调用accept函数还是recv函数
			

select()函数调用及参数、返回值的细节阐述

int select(int maxfd+1, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);
//功能:阻塞等待 一堆 文件描述符,只要有一个数据过来,就解除阻塞
参数:
     maxfd       最大文件描述符 + 1
     read_fds    可能引起阻塞的读相关的文件描述符集(accept recv  recvfrom read)
     write_fds   可能引起阻塞的写相关的文件描述符集(send, sendto write)
     except_fds  可能引起阻塞的其他的文件描述符集
     timeout     NULL  表示一直阻塞,
                 其他值:可以设置阻塞时间
执行完:read_fds中保存引起阻塞解除的文件描述符
read_fds  //read代表的是读相关的文件描述符  //fd代表的是文件描述 //s代表的一个集合,多个文件描述符

int ret = select(maxfd + 1, &readfds, NULL, NULL, NULL);

如何设置read_fds, write_fds, except_fds

fd_set rdfs;    //先定义变量rdfs,这个是一个读文件描述符集合
				//rd --> read //f--> fd // s --> 多个
FD_ZERO(&rdfs);  			//此宏能清空rdfs 列表
FD_SET(fd, &rdfs);        //将文件描述符fd,加入到rdfs中
FD_SET(fd1, &rdfs);        //将文件描述符fd1,加入到rdfs中
FD_CLR(fd1, &rdfs);       //将fd1从rdfs中删除
FD_ISSET(fd1, &current_rdfs)) //判断fd1这个文件描述符,是否在current_rdfs集合存在

//ZERO 0 清空rdfs    //CLEAR 删除fd1
///server.c/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 100
typedef struct sockaddr SA;

int main(int argc, char **argv)
{        
	int sockfd, newsockfd, maxfd, i, nbyte;
	struct sockaddr_in  myaddr;
	char  buf[MAXLINE];
	fd_set global_rdfs, current_rdfs;
    //集合global_rdfs  global 总的 全局的
	//集合current_rdfs current 当前的 
		//创建socket
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror("fail to socket");
		exit(-1);
	}
	printf("sockfd is %d\n",sockfd);
	//将myaddr结构体被bzero函数,初始化为空 struct sockaddr_in  myaddr = { 0 };
	bzero(&myaddr, sizeof(myaddr)); // char buf[100] = "hello "; bzero(buf,sizeof(buf)); memset(buf,0,sizeof(buf));
	myaddr.sin_family      = AF_INET;
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);  //htonl(INADDR_ANY);
	myaddr.sin_port    	   = htons(55555);   /* port number */
    //绑定IP和端口号  
	//SA ---> struct sockaddr
	if(bind(sockfd, (SA *)&myaddr, sizeof(myaddr)) < 0)
	{
		perror("fail to bind");
		exit(-1);
	}
	//监听
	listen(sockfd, 5);
	FD_ZERO(&global_rdfs); //清空
	FD_SET(sockfd, &global_rdfs); //将sockfd 添加到 global_rdfs 中
	maxfd = sockfd; //最大的文件描述符也就是3
	
	while(1)
	{
		current_rdfs = global_rdfs; //global 实际保存  current_rdfs //临时
		// 客户端1连接 ./client 192.168.110.157 55555 连接服务器,可以让select函数解除阻塞
		// 客户端1 hello 回车 可以让select函数解除阻塞
		// 客户端2连接 ./client 192.168.110.157 55555 连接服务器,可以让select函数解除阻塞
		// 客户端2 world 回车 可以让select函数解除阻塞
		if (select(maxfd+1, &current_rdfs, NULL, NULL, 0) < 0) //阻塞等待
		{
			perror("fail to select");
			exit(-1);
		}
		else //解除阻塞了,一但select解除阻塞,那么current_rdfs这个集合里面,只会剩下一个文件描述
			//剩下的这个文件描述符,就是导致解除阻塞原因的描述符
		{
	 		for (i=0; i<=maxfd; i++) //for循环遍历0-maxfd每一个文件描述符,判断是否在集合上current_rdfs中出现(注意select解除阻塞后,current_rdfs中只有一个文件描述符)
	   		{
	    		if (FD_ISSET(i, &current_rdfs))  //判断i这个文件描述符,是否在current_rdfs集合存在
	    		{
			  		if (i == sockfd)  // new connection is coming //有新客户连接请求
					{
						newsockfd = accept(sockfd, NULL, NULL);//new 
						printf("newsockfd is %d\n",newsockfd);
						FD_SET(newsockfd, &global_rdfs);//将newsockfd添加到global_rdfs中
						maxfd = maxfd > newsockfd ? maxfd : newsockfd;
					}
					else  // client send message //客户端发送来消息,引起的解除阻塞
					{
	   					if((nbyte = recv(i, buf, sizeof(buf), 0)) <= 0)//recv返回值 <=0说明客户端断开连接
						{
							close(i); //关闭这个newsockfd
							FD_CLR(i, &global_rdfs);//将这个描述符,从global_rdfs中删除
						}
						else //接收到了数据
						{ 
							printf("recv from client is %s\n",buf);
							send(i, buf, strlen(buf)+1, 0);//把收到的数据回传给客户端
						}
					}
				}
			}  // end for
		}
	}

	return 0;
}

///客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>

void *recv_fun(void *p)
{
	char buf[100] = { 0 };
	int sockfd = *((int *)p);
	while(1)
	{
		if(recv(sockfd,buf,sizeof(buf),0) > 0)
		{
			printf("recv from server is %s\n",buf);
		}
	}
}

int main(int argc, const char *argv[])
{
	if(argc != 3)
	{
		printf("argc != 3 ./client 127.0.0.1 55555\n");
		exit(-1);
	}
	//1.创建socket套接字
	char buf[100] = { 0 };
	struct sockaddr_in toaddr;
	pthread_t id;

	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("sockfd failed:");
		exit(-1);
	}
	toaddr.sin_family = AF_INET;
	toaddr.sin_port = htons(atoi(argv[2]));
	toaddr.sin_addr.s_addr = inet_addr(argv[1]);

	//2.连接服务器
	connect(sockfd,(struct sockaddr *)&toaddr,sizeof(struct sockaddr));

	pthread_create(&id,NULL,recv_fun,&sockfd);
	//3.发送数据
	while(1)
	{
		gets(buf);
		send(sockfd,buf,strlen(buf)+1,0);
	}
	return 0;
}

4.3线程池

       线程池原理:
先把线程创建好,然后线程先休眠,一旦有要执行的任务,唤醒线程   

线程以队列+链表方式进行管理

//线程池的实现//
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <assert.h>
/*组成:
线程池管理 用于创建及管理线程池
工作线程 线程池中的线程--用于处理执行任务
任务接口  提供处理任务的接口 供工作线程调度执行任务
任务队列  存放待处理的任务 工作线程从中取出任务并执行 

线程池里所有运行和等待的任务都是一个Thread_worker
由于所有任务都在链表里,所以是一个链表结构
*/
typedef struct worker
{
    /*回调函数,任务运行时会调用此函数,注意也可声明成其它形式*/
    void *(*process) (void *arg);	//函数指针
    void *arg;/*回调函数的参数*/
    struct worker *next;
} Thread_worker;


//1.线程池结构
typedef struct
{
     pthread_mutex_t queue_lock;
     pthread_cond_t queue_ready;
    /*链表结构,线程池中所有等待任务*/
     Thread_worker *queue_head;//任务1-> 任务2-> 任务3
    int shutdown;  //是否销毁线程池
     pthread_t *threadid;  //线程池中所有线程的tid
    int max_thread_num;  //线程池中允许的活动线程数目
    int cur_queue_size;  //当前等待队列的任务数目
} Thread_pool;

int pool_add_worker (void *(*process) (void *arg), void *arg);
void *thread_routine (void *arg);


static Thread_pool *pool = NULL;
void   pool_init (int max_thread_num)

//2.创建线程池并初始化  参数:线程池最大线程数、任务队列最大任务数 
{
     pool = (Thread_pool *) malloc (sizeof (Thread_pool));
     pthread_mutex_init (&(pool->queue_lock), NULL);
     pthread_cond_init (&(pool->queue_ready), NULL);

     pool->queue_head = NULL;

     pool->max_thread_num = max_thread_num;
     pool->cur_queue_size = 0;

     pool->shutdown = 0;

     pool->threadid =(pthread_t *) malloc (max_thread_num * sizeof (pthread_t)); 
     int i = 0;
     for (i = 0; i < max_thread_num; i++)//批量创建线程
     { 
         pthread_create (&(pool->threadid[i]), NULL, thread_routine,NULL);
     }
}

//3. 向线程池中加入任务  向任务队列中添加任务  //参数 任务指针  
int pool_add_worker (void *(*process) (void *arg), void *arg)
{
    /*构造一个新任务*/
     Thread_worker *newworker =(Thread_worker *) malloc (sizeof (Thread_worker));
     newworker->process = process;
     newworker->arg = arg;
     newworker->next = NULL;

     pthread_mutex_lock (&(pool->queue_lock));
    /*将任务加入到等待队列中*/
     Thread_worker *member = pool->queue_head;
     if (member != NULL)//对列 尾插 执行任务 头删
     {
        while (member->next != NULL)
             member = member->next;
             
         member->next = newworker;
     }
     else
     {
         pool->queue_head = newworker;
     }

     pool->cur_queue_size++;
     pthread_mutex_unlock (&(pool->queue_lock));
     //唤醒一个等待线程 如果所有线程都在忙碌,这句没有任何作用
     // 唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。
     //如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用。

     pthread_cond_signal (&(pool->queue_ready));	//发送信号给正在阻塞的线程,阻塞的线程解除阻塞,执行process
     return 0;
}

//4. 销毁线程池,等待队列中的任务不会再被执行,但是正在运行的线程会一直把任务运行完后再退出
int pool_destroy ()
{
    if (pool->shutdown)
        return -1;/*防止两次调用*/
     pool->shutdown = 1;

     //唤醒所有等待线程,线程池要销毁
     pthread_cond_broadcast (&(pool->queue_ready));

     /*阻塞等待线程退出,否则就成僵尸了*/
     int i;
     for (i = 0; i < pool->max_thread_num; i++)
     { 
	pthread_join (pool->threadid[i], NULL);
     }
     free (pool->threadid);

     /*销毁等待队列*/
     Thread_worker *head = NULL;
     while (pool->queue_head != NULL)
     {
         head = pool->queue_head;
         pool->queue_head = pool->queue_head->next;
         free (head);
     }
     /*条件变量和互斥量也别忘了销毁*/
     pthread_mutex_destroy(&(pool->queue_lock));
     pthread_cond_destroy(&(pool->queue_ready));
    
     free (pool);
     /*销毁后指针置空是个好习惯*/
     pool=NULL;
     return 0;
}

//5. 工作线程的主要工作为处理任务,当任务队列不为空的时候,工作线程直接从队列头取出一个任务并执行;
//当任务队列为空的时候,工作线程将阻塞直到有任务添加进来;
void *thread_routine (void *arg)
{
     printf ("starting thread 0x%x\n", (unsigned int)pthread_self ());
     while (1)
     {
         pthread_mutex_lock (&(pool->queue_lock));
         //如果等待队列为0并且不销毁线程池,则处于阻塞状态
         while (pool->cur_queue_size == 0 && !pool->shutdown)
         {
             printf ("thread 0x%x is waiting\n", (unsigned int)pthread_self ());
             pthread_cond_wait (&(pool->queue_ready), &(pool->queue_lock));		//让线程休眠
         }

         //线程池销毁
         if (pool->shutdown)
         {
             pthread_mutex_unlock (&(pool->queue_lock));
             printf ("thread 0x%x will exit\n", (unsigned int)pthread_self ());
             pthread_exit (NULL);
         }

         printf ("thread 0x%x is starting to work\n", (unsigned int)pthread_self ());
        
         //等待队列长度减去1,并取出链表中的头元素
         pool->cur_queue_size--;
         Thread_worker *worker = pool->queue_head;//头删 执行任务
         pool->queue_head = worker->next;
         pthread_mutex_unlock (&(pool->queue_lock));

         /*调用回调函数,执行任务*/
         (*(worker->process)) (worker->arg);
         free (worker);
         worker = NULL;
     }
     pthread_exit (NULL);
}
   
void *myprocess (void *arg)
{
     printf ("threadid is 0x%x, working on task %d\n", (unsigned int)pthread_self (),*(int *) arg);
     sleep (1);
     return NULL;
}

int  main (int argc, char **argv)
{
     pool_init (3);	//创建3个线程,默认是休眠的
     int *workingnum = (int *) malloc (sizeof (int) * 10);
     int i;
     for (i = 0; i < 10; i++)
     {
         workingnum[i] = i;
         pool_add_worker(myprocess, &workingnum[i]);//给线程添加任务,执行myprocess
     }
     sleep (5);
     pool_destroy ();//销毁线程
     free (workingnum);
     return 0;
}

五、单播 组播 广播

5.1 广播(UDP)

UDP广播原理及操作步骤:

单播 组播 广播

1. 广播:通过udp广播数据(只有udp可以广播,因为tcp需要连接)

广播地址一定 
192.168.30.255   //255就表示是广播地址

BroadCast //关播

//接收端: 原来绑定的是自己电脑的IP地址和端口号,现在绑定的是广播地址和端口号
//发送端: 1. 原来发送之前指定的是接收方的IP地址和端口号,现在我们指定的是广播的地址和端口号
		  2. setsockopt 允许发送关播

分为发送端和接收端:
发送端:
	(1)创建一个UDP数据报套接字 
		int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	(2)指定目标的IP地址和端口号(192.168.110.255 8888)
		struct sockaddr_in toaddr = { 0 };
		toaddr.sin_family = AF_INET;
		toaddr.sin_port = htons(8888);
		toaddr.sin_addr.s_addr = inet_addr("192.168.110.255");
	(3)设置套接字选项允许发送广播 
		int on = 1; //1代表允许发送广播,0代表不运行
		setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
		//setsockopt 
		set 设置 	sock ----socket 套接字 opt  ---- option选择
	(4)发送数据包
		char buf[100] = "hello world";
		sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&toaddr,sizeof(toaddr));
接收端 
	(1)创建UDP套接字 
		int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	(2)绑定广播地址和端口号
		struct sockaddr_in myaddr = { 0 };
		myaddr.sin_family = AF_INET;
		myaddr.sin_port = htons(8888);
		myaddr.sin_addr.s_addr = inet_addr("192.168.110.255");
		
		bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
	(3)接收数据
		struct sockaddr_in youaddr = { 0 }; //用来保存发送方的IP地址和端口号
		int len = sizeof(youaddr);
		recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&youaddr,&len);
		recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL); //不想知道发送方的IP地址和端口号
	(4) 关闭套接字
		close(sockfd);

代码案例:

/广播接收端recv.c
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };
	//1.创建一个数据报套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.绑定广播地址和端口号
	struct sockaddr_in broadaddr = { 0 };
	broadaddr.sin_family = AF_INET;
	broadaddr.sin_port = htons(7777);
	broadaddr.sin_addr.s_addr = inet_addr("192.168.110.255");
	if(bind(sockfd,(struct sockaddr*)&broadaddr,sizeof(broadaddr)) == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind ok!!\n");
	//3.接收广播的数据
	while(1)
	{
		recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
		printf("from broadcast: %s\n",buf);
	}
	//4.关闭套接字
	close(sockfd);

	return 0;
}

/广播发送端send.c
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };
	//1.创建一个数据报套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.允许发送广播
	int on = 1;//1允许发送广播
	setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));

	//3.指定广播的地址和端口号
	struct sockaddr_in toaddr = { 0 };
	toaddr.sin_family = AF_INET;
	toaddr.sin_port = htons(7777);
	toaddr.sin_addr.s_addr = inet_addr("192.168.110.255");
	while(1)
	{
		printf("请输入要广播的话:\n");
		gets(buf);
		sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&toaddr, sizeof(toaddr));
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

5.2 IP地址类别、子网掩码

可以给1组计算机发送数据;只要有一台计算机加入到一个组里面,就可以给这个计算机发送数据

IP地址的组成 
	IP地址是4个字节,32位 192.168.1.21 
	in_addr_t ----- unsigned int 
	
	1个字节 == 8bit
	1个无符号的int,表达最大的数是几??	1111 1111 ----> 255

	下列哪些地址,不可以作为IP地址 
	A 主机: 192.168.1.256   //不可以 256>255       
	B 主机: 192.168.1.255   //不可以 广播地址
			192.168.1.(1111 1111)
	C 主机: 192.168.1.0   	//不可以 全是0,也不行	
			192.168.1.(0000 0000)
	分得的主机号 1 - 254
	IP地址分为两部分,网络地址 和 主机地址 	共32位

IP地址:
(1)A类地址  
	8位网络地址 24位主机地址 
	最高位是0,7位有效网络地址	

	24位主机地址(2^24-2 个主机)
	1.0.0.1 --- 126.255.255.254
	
(2)B类地址  
	16位网络地址 16位主机地址 
	最高位是10, 14位有效网络地址 

	16位主机地址(2^16-2 个主机)
	128.0.0.1 --- 191.255.255.254	
		网络地址 				 主机地址	
	128.0.0.1
	1000 0000 0000 0000     0000 0000 0000 0001
	191.255.255.254
	1011 1111 1111 1111     1111 1111 1111 1110

(3)C类地址 
	24位网络地址 8位主机地址	
	最高位是110, 21位有效网络地址 
	
	8位主机地址(2^8-2 个主机)
	192.0.0.1 --- 223.255.255.254
			网络地址 				 主机地址
	192.0.0.1
	1100 0000 0000 0000 0000 0000    0000 0001
	223.255.255.254
	1101 1111 1111 1111 1111 1111    1111 1110	
	
	2的8次幂种组合 0-255
				   0000 0000 
				   1111 1111
	能够分配的IP地址个数 
	2^8 - 2   == 254个 
			0000 0000 //不能用
			1111 1111 //不能用 广播 
				   1 - 254
(4)D 类地址 最高位为 1110 专门用来做(多播)组播
	最高位是1110
	224.0.0.1 -----239.255.255.254
	
	1110 0000  --> 224
	1110 1111  --> 239
ip地址类别总结:
A类: 0开头 
B类: 10开头 
C类: 110开头 
D类: 1110开头 专门用来做组播的

//如何区分是哪一类的IP地址,就看这个IP地址是以谁为开头
127开头的ip地址
127.0.0.0到127.255.255.255是保留地址,用做循环测试用的。(自发自收    127.0.0.1)

####################################################################################
子网掩码定义: 用来指明一个IP地址的哪些位标识的是主机地址
	作用:用来将一个大网络分成几个子网
	子网掩码都是 左1右0
	1的部分是网络地址	0的部分是主机地址
255.255.255.0----> 网络地址24位,主机地址8位
		网络地址				  主机地址

1111 1111 1111 1111 1111 1111   0000 0000 
192.168.1.1 <-------------> 192.168.1.2 是否属于同一网络 //属于同一个网络

192.168.1.1 // 前24位网络地址 192.168.1
192.168.1.2 // 前24位网络地址 192.168.1

192.168.0.1 <-------------> 192.168.1.2 是否属于同一网络 //不属于统一网络
192.168.0.1 //前24位网络地址 192.168.0
192.168.1.2 //前24位网络地址 192.168.1

当设置完子网掩码之后,如果是和我同一个局域网的主机,通信(不通过网关)

否则(需要通过网关)
网关: 当访问另一个网络中的主机时,就要通过网关(通常是路由器)

//问题 
子网掩码 255.255.255.0 那么此网络中最多有多少台主机?  
	//解题方法:
	(1)先通过子网掩码 得到主机地址占多少位 
		主机地址占 8位
	(2)最多的主机个数是 2^8 - 2  //2的8次幂再-2

  
子网掩码 255.255.255.240 那么此网络中最多有多少台主机?
   1111 1111 1111 1111 1111 1111 1111 0000
	//解题方法:
	(1)先通过子网掩码 得到主机地址占多少位
		主机地址占 4位
	(2)最多的主机个数是 2^4 - 2 = 14 //2的4次幂再-2

5.3 组播(UDP)

主播相关函数调用及函数参数详解:

组播步骤(UDP)MultiCast //组播
最高位是1110      224.0.0.1 -----239.255.255.254
发送端和接收端 	
发送端:
	(1)创建一个UDP套接字 
		int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	(2)指定组播地址和端口号 
		struct sockaddr_in multiaddr = { 0 }; //用来保存指定的组播地址
		
		multiaddr.sin_family = AF_INET;
		multiaddr.sin_port = htons(6666);
		multiaddr.sin_addr.s_addr = inet_addr("224.10.10.10"); //注意此处为组播地址
	(3)发送数据包 
		char buf[100] = "hello";
		sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&multiaddr,sizeof(multiaddr));
	(4)关闭套接字 
		close(sockfd);
		
	broadcast 广播 	multicast 组播
接收端:
	(1)创建一个UDP套接字
		int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	(2)将本机地址和组播地址关联在一起
		struct ip_mreq mreq = { 0 } //里面用来保存组播地址和本机地址
		
		mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.10"); //组播地址
		mreq.imr_interface.s_addr = htonl(INADDR_ANY);//本机地址
		
		setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
		//执行此函数后,才将本机地址与组播地址关联在一起
	(3)绑定组播地址及端口号
		struct sockaddr_in multiaddr = { 0 };
		
		multiaddr.sin_family = AF_INET;
		multiaddr.sin_port = htons(6666);
		multiaddr.sin_addr.s_addr = inet_addr("224.10.10.10");
		
		bind(sockfd,(struct sockaddr *)&multiaddr,sizeof(multiaddr));
	(4)接收数据 
		struct sockaddr_in youaddr = { 0 }; //用来保存发送方的IP地址和端口号
		int len = sizeof(youaddr);
	
		recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&youaddr,&len);
	(5)关闭套接字 
		close(sockfd);

组播代码案例:

组播接收端 recv.c///
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };
	//1.创建一个数据报套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}
	//2.将组播的地址和本机地址关联在一起
	struct ip_mreq mreq = { 0 };
	mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.10");//组播地址
	mreq.imr_interface.s_addr = htonl(INADDR_ANY);//本机地址
	setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));//组播和本机关联在一起

	//3.绑定组播地址和端口号
	struct sockaddr_in multiaddr = { 0 };
	multiaddr.sin_family = AF_INET;
	multiaddr.sin_port = htons(7777);
	multiaddr.sin_addr.s_addr = inet_addr("224.10.10.10");
	if(bind(sockfd,(struct sockaddr*)&multiaddr,sizeof(multiaddr)) == -1)
	{
		perror("bind failed");
		exit(-1);
	}
	printf("bind ok!!\n");

	//4.接收组播的数据
	while(1)
	{
		recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
		printf("from multicast: %s\n",buf);
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

组播发送端 send.c///
#include "my.h"

int main(int argc, const char *argv[])
{
	char buf[100] = { 0 };
	//1.创建一个数据报套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sockfd == -1)
	{
		perror("socket failed");
		exit(-1);
	}

	//2.指定组播的地址和端口号
	struct sockaddr_in multiaddr = { 0 };
	multiaddr.sin_family = AF_INET;
	multiaddr.sin_port = htons(7777);
	multiaddr.sin_addr.s_addr = inet_addr("224.10.10.10");
	while(1)
	{
		printf("请输入要组播的话:\n");
		gets(buf);
		sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&multiaddr, sizeof(multiaddr));
	}
	//4.关闭套接字
	close(sockfd);
	return 0;
}

总结

这里对文章进行总结:

主要是整理了网络体系结构(OSI 七层模型、TCP/IP 四层模型、IP地址(点分制、整型)及字节序转序、端口号);UDP及TCP通信(发送与接受端、服务器与客户端);循环服务器模型(多线程并发服务器、I/O多路复用与线程池);组播及广播的学习笔记,

下篇则是sqlite数据库相关知识。

九层之台,起于垒土!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值