目录
前言
网络编程被分为上下两篇,上篇将整理网络体系结构(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(¤t_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, ¤t_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, ¤t_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, ¤t_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数据库相关知识。
九层之台,起于垒土!!