目录
前言
前面编写的进程间通信:只能用于同一台主机内部不同的进程,而这次讲诉的网络编程也是进程间通信的一种方式,它可以用于不同主机间的进程通信。
一、基础理论
1. 网络分层模型
(1)OSI七层模型
作用:为了方便大家去理解网络通信的整个流程,人为地把计算机网络划分为七个层次。
划分标准:
- 应用层 :网络服务与最终用户的一个接口,需要用到这个层次中对应的通信协议,http协议(超文本传输协议,开发网页) ftp协议(文件传输协议) telnet协议(远程登录)等。
- 表示层 :数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)格式有,JPEG、ASCll、EBCDIC、加密格式等。
- 会话层:建立、管理、终止会话。(在五层模型里面已经合并到了应用层),对应主机进程,指本地主机与远程主机正在进行的会话。
- 传输层:定义传输数据的协议端口号,以及流控和差错校验。协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层
- 网络层:找到数据传输的最优路径(路由功能) 协议有:ICMP IGMP IP(IPV4 IPV6)。
- 数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验 等功能。(由底层网络定义协议)。
- 物理层:建立、维护、断开物理连接。(由底层网络定义协议)。
网络协议:老外给七个层次,每个层次都制定了很多通信协议(网络协议,游戏规则)
(2)TCP/IP模型
把七层模型简化成了四层,有些资料也会说 TCP/IP 是五层模型,所谓的五层模型指的是在数据链路层下面还有一个物理层,而作为软件工程师一般不需要关注物理层,所以通常我们说 TCP/IP 四层。
2. ipv4和ipv6
- ipv4地址:32位的地址 文本格式为 nnn.nnn.nnn.nnn,其中 0<=nnn<=255,比如:192.168.24.2 (点分十进制ip,三个小数点划分为4个部分,每个部分各占一个字节)
- ipv6地址:解决ipv4不够用,扩展了位数,扩展到128位,文本格式为 xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx,其中每个 x 都是十六进制数,表示 4 位。
3. 端口号
作用:区分同一台主机内部不同的网络进程
特点:同一台主机内部,端口号不能一样,不同主机,端口号可以是一样的
本质:无符号短整型数字,取值范围0---65535之间,程序员是可以给程序指定端口号,但是需要注意,1024以内的端口号不要去使用(1024以内的端口号很多被linux系统占用)。
二、使用实例
1.TCP协议
1.tcp协议的通信流程
2、相关API
(1)创建套接字
int socket(int domain, int type, int protocol);
返回值:成功 返回套接字的文件描述符,失败 -1
参数:domain --》地址协议
IPV4地址协议 --》AF_INET或者PF_INET
IPV6地址协议 --》AF_INET6或者PF_INET6
type --》套接字类型
tcp套接字(数据流套接字,流式套接字) --》SOCK_STREAM
udp套接字(数据报套接字) --》SOCK_DGRAM
protocol --》扩展协议,一般默认设置为0(2)绑定ip和端口号(自己的ip和端口号)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //兼容ipv4和ipv6
int bind(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen); //只能使用IPv4地址
int bind(int sockfd, const struct sockaddr_in6 *addr, socklen_t addrlen); //只能使用IPv6地址返回值:0时表示绑定成功,-1表示绑定失败
参数: sockfd --》套接字的文件描述符
addr --》存放你要绑定的ip和端口号
struct sockaddr //通用地址结构体,兼容ipv4和ipv6
{
sa_family_t sa_family; //保存地址协议 2字节
char sa_data[14]; //存放ip和端口号 14字节
} 16字节
struct sockaddr_in // IPV4地址结构体
{
short sin_family; //地址协议
struct in_addr sin_addr; //存放要绑定的ip地址 4字节
short sin_port; //存放要绑定的端口号
sin_zero; //打酱油的,保证struct sockaddr_in大小跟struct sockaddr一致
}
struct in_addr
{
in_addr_t s_addr; //最终存放要绑定的ip地址
}
struct sockaddr_in6 //IPV6地址结构体
{
}
addrlen --》地址结构体的大小 sizeof()
(3)连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值:如果请求连接成功,则返回0,否则返回错误码
参数:addr --》存放服务器的ip和端口号
(4)监听--tcp协议允许多个客户端连接同一个服务器
int listen(int sockfd, int backlog);
返回值:成功返回0,失败返回
SOCKET_ERROR
参数:backlog(重点) --》最多允许多少个客户端同时连接一台服务器
如:listen(sockfd,6);(5)接受客户端的连接请求,愿意接听
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回新的套接字文件描述符,失败 -1
特点:(重点)
每连接一个客户端,accept都会产生新的套接字
新的套接字用来跟客户端通信
accept如果没有客户端连接,会一直阻塞,一旦有客户端连接成功,accept会解除阻塞,返回新的套接字参数:addr --》存放目前连接成功的客户端的ip和端口号,该变量不需要初始化
addrlen --》地址长度,要求是指针(6)读函数
int read(int sockfd, void *buf, size_t count);
int recv(int sockfd, void *buf, size_t len, int flags);详细讲解:请点击
返回值:1、成功,返回读到的字节数
2、如果读取时已经到达文件的末尾或者另一端已关闭则返回0。
3、如果出错,则返回-1
参数--》 sockfd 套接字
buf 存放数据的指针
count 想要读取的数据长度
flags 一组影响此函数行为的标志,一般设为0
(7)写函数
int write(int fd, const void *buf, size_t count);
int send(int sockfd, const void *buf, size_t len, int flags);详细讲解:请点击
返回值:
1.大于0
(a)等于给定字节数count
(b)小于给定字节数count,有如下几种可能:
- 底层物理介质上没有足够的空间
- 创建的文件指定了RLIMIT_FSIZE,也就是指定了文件允许的最大字节数,不能再往其中添加数据
- 已经写了部分数据,但是被中断信号打断,返回中断打断前写入的字节数。
2.返回值=0
如果相应的errno被设定,说明有相应失败情况。如果errno没有被设定,没有任何影响(可能是write 指定写入0字节等)
3.返回值<0(-1)
出错,查看errno:
- EAGAIN or EWOULDBLOCK:fd被设定为非阻塞,并且write将会被阻塞,立即返回-1,errno为EAGAIN
- EINTR:a、阻塞fd:被一个信号打断,但是需要强调的是,在信号打断前没有写入一个字节,才会返回-1,errno设定为EINTR。如果有写入,返回已经写入的字节数。这其实很好理解,如果写入了部分数据依然返回-1,errno设定为EINTR,处理完中断后,由于不知道被打断时写到了什么地方,也就不知道该从哪一个地方继续写入。b、非阻塞fd:调用非阻塞write,即使write被信号打断,write会会继续执行未完成的任务而不会去响应信号。因为在非阻塞调用中,没有任何理由阻止read或者wirte的执行。
- EPIPE:fd是一个pipe或者socket,而对端的读端关闭。但是一般而言,写进程会收到SIGPIPE信号。(注意:和read不一样,read对端关闭使返回0)
(8)关闭文件描述符或套接字
int close(int fd)
返回值:关闭成功返回0,失败则返回-1.
2.代码实例
2.1 客户端双向通信代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
/*
tcp客户端代码--》男朋友
双向通信
*/
#define RECV_BUF 1024
#define SEND_BUF 1024
int tcpsock;
void *recvservermsg(void *arg)
{
char otherbuf[RECV_BUF];
int ret;
while(1)
{
bzero(otherbuf,RECV_BUF);
ret = read(tcpsock,otherbuf,RECV_BUF);
if(ret==0)
break;
printf("服务器发送过来的信息是:%s\n",otherbuf);
}
}
int main(int argc,char **argv)
{
int ret;
pthread_t id;
char buf[SEND_BUF];
//定义结构体变量存放对方(服务器)的ip和端口号
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //服务器的ip地址
serveraddr.sin_port = htons(atoi(argv[2])); //服务器的端口号
//创建tcp套接字
tcpsock = socket(AF_INET,SOCK_STREAM,0);
if(tcpsock==-1)
{
perror("创建tcp套接字!\n");
return -1;
}
//设置端口重复使用
int on = 1;
setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//连接服务器
ret = connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret==-1)
{
perror("连接服务器!\n");
return -1;
}
//创建一个子线程,接收服务器回复的信息
pthread_create(&id,NULL,recvservermsg,NULL);
//发送信息给服务器,主线程发送
while(1)
{
bzero(buf,SEND_BUF);
printf("请输入要发送给服务器的信息!\n");
scanf("%s",buf);
write(tcpsock,buf,strlen(buf));
}
close(tcpsock);
return 0;
}
2.2 服务器双向通信代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#define RECV_BUF 1024
#define SEND_BUF 1024
int newsock;
void *sendmsgclient(void *arg)
{
char otherbuf[SEND_BUF];
while(1)
{
bzero(otherbuf,SEND_BUF);
printf("请输入要发送给客户端的信息!\n");
scanf("%s",otherbuf);
write(newsock,otherbuf,strlen(otherbuf));
}
}
int main(int argc,char **argv)
{
int tcpsock;
int ret;
pthread_t id;
char buf[RECV_BUF];
//定义ipv4地址结构体变量
struct sockaddr_in bindaddr;
bzero(&bindaddr,sizeof(bindaddr));
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本机任意IP
bindaddr.sin_port = htons(atoi(argv[1])); //绑定服务器自己的端口号
struct sockaddr_in clientaddr;
bzero(&clientaddr,sizeof(clientaddr));
int addrsize=sizeof(clientaddr);
//创建tcp套接字
tcpsock = socket(AF_INET,SOCK_STREAM,0);
if(tcpsock==-1)
{
perror("创建tcp套接字!\n");
return -1;
}
printf("旧的文件描述符:%d\n",tcpsock);
//设置端口重复使用
int on = 1;
setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//绑定ip和端口号
ret = bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
if(ret==-1)
{
perror("绑定失败!\n");
return -1;
}
//监听
ret = listen(tcpsock,8);
if(ret==-1)
{
perror("监听失败!\n");
return -1;
}
//接收客户端的连接请求
newsock = accept(tcpsock,(struct sockaddr *)&clientaddr,&addrsize);
if(newsock==-1)
{
perror("接收客户端的连接请求失败!\n");
return -1;
}
printf("新的文件描述符:%d\n",newsock);
//创建一个线程专门发送信息给客户端
pthread_create(&id,NULL,sendmsgclient,NULL);
//接收信息
while(1)
{
bzero(buf,RECV_BUF);
ret = read(newsock,buf,RECV_BUF);
if(ret==0)
break;
printf("read的返回值是:%d\n",ret);
printf("客户端发送给我的信息是:%s\n",buf);
}
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了网络编程的基础知识和简单的使用案例。