网络编程1 基础概念+Socket编程
网络编程1 基础概念+Socket编程
协议
概念: 协议事先约定好, 大家共同遵守的一组规则, 如交通信号灯.
从应用程序的角度看, 协议可理解为数据传输和数据解释的规则;
可以简单的理解为各个主机之间进行通信所使用的共同语言.
分层模型
OSI是Open System Interconnection的缩写, 意为开放式系统互联. 国际标准化组织(ISO)制定了OSI模型, 该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架.
网络分层 OSI 7层模型: 物数网传会表应
- 物理层---双绞线,光纤(传输介质),将模拟信号转换为数字信号
- 数据链路层---数据校验,定义了网络传输的基本单位-帧
- 网络层---定义网络,两台机器之间传输的路径选择点到点的传输
- 传输层---传输数据 TCP,UDP,端到端的传输
- 会话层---通过传输层建立数据传输的通道.
- 表示层---编解码,翻译工作.
- 应用层---为客户提供各种应用服务,email服务,ftp服务,ssh服务
数据通信过程
通信过程: 其实就是发送端层层打包, 接收方层层解包.
注意: 这些操作不是用户自己做的, 而是底层帮我们做好的.
网络应用程序的设计模式
BS模式 CS模式
-
CS设计模式优缺点:
优点:
客户端在本机上可以保证性能, 可以将数据缓存到本地, 提高数据的传输效率, 提高用户体验效果.
客户端和服务端程序都是由同一个开发团队开发, 协议选择比较灵活.
缺点:
服务器和客户端都需要开发,工作量相对较大, 调试困难, 开发周期长;
从用户的角度看, 需要将客户端安装到用户的主机上, 对用户主机的安 全构成威胁. -
BS设计模式优缺点:
优点:
无需安装客户端, 可以使用标准的浏览器作为客户端;
只需要开发服务器,工作量相对较小;
由于采用标准的客户端, 所以移植性好, 不受平台限制.
相对安全,不用安装软件
缺点:
由于没有客户端, 数据缓冲不尽人意, 数据传输有限制, 用户体验较差;
通信协议选择只能使用HTTP协议,协议选择不够灵活
ARP协议 TCP协议 UDP协议
ARP协议 IP段
UDP数据报格式
TCP数据流格式
在计算机网络中,大端和小端指的是多字节数据类型的存储方式。
数据的低位字节是指数据中数值较小的字节,而内存的高位地址是指内存中地址值较大的地址。
内存地址是指内存中某个存储单元的编号。每个存储单元都具有唯一的地址,地址从 0 开始递增。
在计算机中,内存地址通常由 32 位或 64 位二进制位组成。地址的高位表示内存中哪个存储单元组,低位表示该组内的哪个存储单元。
例如,假设内存地址为 0x12345678,其中:
0x1234 是高位,表示内存中第 0x1234 个存储单元组
5678 是低位,表示该组内的第 5678 个存储单元
**大端(Big Endian)是指高位字节存放到低位地址,低位字节存放到高位地址。
**小端(Little Endian)则是指低位字节存放到低位地址,高位字节存放到高位地址。
下面4个函数就是进行大小端转换的函数:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
函数名的h表示主机host, n表示网络network, s表示short, l表示long
上述的几个函数, 如果本来不需要转换函数内部就不会做转换.
P地址转换函数:
p->表示点分十进制的字符串形式
to->到
n->表示network网络
**int inet_pton(int af, const char src, void dst);
函数说明: 将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
参数说明:
af: AF_INET
src: 字符串形式的点分十进制的IP地址
dst: 存放转换后的变量的地址
例如: inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
手工也可以计算: 如192.168.232.145, 先将4个正数分别转换为16进制数,
192--->0xC0 168--->0xA8 232--->0xE8 145--->0x91
最后按照大端字节序存放: 0x91E8A8C0, 这个就是4字节的整形值.
**const char *inet_ntop(int af, const void src, char dst, socklen_t size);
函数说明: 网络IP转换为字符串形式的点分十进制的IP
参数说明:
af: AF_INET
src: 网络的整形的IP地址
dst: 转换后的IP地址,一般为字符串数组
size: dst的长度
返回值:
成功--返回指向dst的指针
失败--返回NULL, 并设置errno
Socket编程
传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信.( 本质上借助内核-内核提供了socket伪文件的机制实现通信----实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库.
socket编程用到的重要的结构体:struct sockaddr
struct sockaddr结构说明:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in结构:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET /
in_port_t sin_port; / port in network byte order /
struct in_addr sin_addr; / internet address */
};
** /* Internet address. /*
struct in_addr {
uint32_t s_addr; /* address in network byte order */
}; //网络字节序IP--大端模式
socket编程主要的API函数介绍
**int socket(int domain, int type, int protocol); **
函数描述: 创建socket
参数说明:
domain: 协议版本
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX AF_LOCAL本地套接字使用
type:协议类型
SOCK_STREAM 流式, 默认使用的协议是TCP协议
SOCK_DGRAM 报式, 默认使用的是UDP协议
protocal:
一般填0, 表示使用对应类型的默认协议.
返回值:
成功: 返回一个大于0的文件描述符
失败: 返回-1, 并设置errno
当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列.
*int bind(int sockfd, const struct sockaddr addr, socklen_t addrlen);
函数描述: 将socket文件描述符和IP,PORT绑定
参数说明:
socket: 调用socket函数返回的文件描述符
addr: 本地服务器的IP地址和PORT,
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 表示使用本机任意有效的可用IP
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
addrlen: addr变量的占用的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
int listen(int sockfd, int backlog);
函数描述: 将套接字由主动态变为被动态
参数说明:
sockfd: 调用socket函数返回的文件描述符
backlog: 同时请求连接的最大个数(还未建立连接)
返回值:
成功: 返回0
失败: 返回-1, 并设置errno
**int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); **
函数说明:获得一个连接, 若当前没有连接则会阻塞等待.
函数参数:
sockfd: 调用socket函数返回的文件描述符
addr: 传出参数, 保存客户端的地址信息
addrlen: 传入传出参数, addr变量所占内存空间大小
返回值:
成功: 返回一个新的文件描述符,用于和客户端通信
失败: 返回-1, 并设置errno值.
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
*int connect(int sockfd, const struct sockaddr addr, socklen_t addrlen);
函数说明: 连接服务器
函数参数:
sockfd: 调用socket函数返回的文件描述符
addr: 服务端的地址信息
addrlen: addr变量的内存大小
返回值:
成功: 返回0
失败: 返回-1, 并设置errno值
接下来就可以使用write和read函数进行读写操作了.
除了使用read/write函数以外, 还可以使用recv和send函数
读取数据和发送数据:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
对应recv和send这两个函数flags直接填0就可以了.
注意: 如果写缓冲区已满, write也会阻塞, read读操作的时候, 若读缓冲区没有数据会引起阻塞.
服务端和客户端开发流程
服务端开发流程
- 1 创建Socket,返回一个文件描述符lfd----socket() 该文件描述符用于监听客户端连接
- 2 将lfd和IP PORT 进行绑定-----bind()
- 3 将lfd由主动转被动监听-------listen()
- 4 接受一个新连接,得到一个新文件描述符cfd, accept()该文件描述符用于和客户端进行通信
- 5 while(1)
- 6 关闭文件描述符 ------close(lfd) close(cfd)
服务端代码
//服务端程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
int main()
{
//创建socket
//int socket(int domain, int type, int protocol);
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd<0)
{
perror("socket error");
return -1;
}
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//绑定
struct sockaddr_in serv;
bzero(&serv, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用IP
int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("bind error");
return -1;
}
//监听
//int listen(int sockfd, int backlog);
listen(lfd, 128);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(lfd, (struct sockaddr *)&client, &len); //len是一个输入输出参数
//const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//获取client端的IP和端口
char sIP[16];
memset(sIP, 0x00, sizeof(sIP));
printf("client-->IP:[%s],PORT:[%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
printf("lfd==[%d], cfd==[%d]\n", lfd, cfd);
int i = 0;
int n = 0;
char buf[1024];
while(1)
{
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
for(i=0; i<n; i++)
{
buf[i] = toupper(buf[i]);
}
//发送数据
write(cfd, buf, n);
}
//关闭监听文件描述符和通信文件描述符
close(lfd);
close(cfd);
return 0;
}
编译后运行服务器端,在另外一个端口使用命令:
nc 127.0.0.1 8888
服务器端口
lfd==[3], cfd==[4]
模拟服务端
hello
nihaowodeLinuxhhh
服务器端口
n==[6], buf==[hello
]
n==[18], buf==[nihaowodeLinuxhhh
]
客户端开发流程
- 1创建socket,返回一个文件描述符cfd ------socket()
- 2连接服务端 connect()
- 3while(1)
- 4close(cfd)
客户端代码
//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
//创建socket---用于和服务端进行通信
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd<0)
{
perror("socket error");
return -1;
}
//连接服务端
//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
printf("[%x]\n", serv.sin_addr.s_addr);
int ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("connect error");
return -1;
}
int n = 0;
char buf[256];
while(1)
{
//读标准输入数据
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
//发送数据
write(cfd, buf, n);
//读服务端发来的数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or server closed, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
}
//关闭套接字cfd
close(cfd);
return 0;
}
服务器端和客户端验证
一些细节,调用accept函数并不是新建一个连接,而是从已连接队列中取出一个可用连接。