- TCP三次握手建立连接
- 错误处理模块:wrap.c,函数声明:wrap.h
- 并发服务器模型(多进程,多线程)
转换大小写程序
服务端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#define PORT 6799
#define IP "127.0.0.1"
void print_err(char* str){perror(str);exit(-1);
}
int main(){int res_bind=0;int cfd=0;struct sockaddr_in serverAddr;struct sockaddr_in clientAddr;socklen_t cli_addr_len;int sfd=socket(AF_INET,SOCK_STREAM,0);if(sfd==-1) print_err("socket fails\n");//把内存清零,在使用结构体赋值前将缓冲区清零bzero(&serverAddr,sizeof(serverAddr));serverAddr.sin_family=AF_INET;serverAddr.sin_port=htons(PORT);//serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);inet_pton(sfd,IP,&serverAddr.sin_addr.s_addr);res_bind=bind(sfd,(struct sockaddr*)&serverAddr,\sizeof(serverAddr));if(res_bind==-1) print_err("bind fails\n");listen(sfd,120);cli_addr_len=sizeof(clientAddr);cfd=accept(sfd,(struct sockaddr*)&clientAddr,&cli_addr_len);if(cfd==-1) print_err("accept fails\n");/*显示一下哪个客户端连接了*/char client_IP[100]={0};printf("client IP=%s,client PORT=%d\n",\inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,\client_IP,sizeof(client_IP)),\ntohs(clientAddr.sin_port));/*转换大小写*/char buf[30]={0};int n=read(cfd,buf,sizeof(buf));int i;for(i=0;i<n;i++){buf[i]=toupper(buf[i]);}write(cfd,buf,sizeof(buf));close(sfd);close(cfd);return 0;
}
用户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#define PORT 6799
#define IP "127.0.0.1"
void print_err(char* str){perror(str);exit(-1);
}
int main(){int cfd=socket(AF_INET,SOCK_STREAM,0);if(cfd==-1) print_err("socket fails\n");struct sockaddr_in server_addr;int connect_res=0;memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);inet_pton(AF_INET,IP,&server_addr.sin_addr.s_addr);connect_res=connect(cfd,(struct sockaddr*)&server_addr,\sizeof(server_addr));if(connect_res==-1) print_err("connect fails\n");/*执行程序*///把用户输入读到bufchar buf[100]={0};fgets(buf,sizeof(buf),stdin);//把buf内容写到用户端write(cfd,buf,strlen(buf));//把返回结果写回屏幕int n=read(cfd,buf,sizeof(buf));write(1,buf,sizeof(buf));close(cfd);return 0;
}
执行结果
程序分析
- 每次创建出一个socket,
socket的描述符都指向两个缓冲区
,一个用来读,一个用来写 - 两个套接字想进行网络通信,必须通过
ip地址+端口号
,才能建立网络连接
套接字的读写缓冲区在
内核
中定义,所以用户定义的缓冲区,需要借助write和read进行读写用户定义的缓冲区在栈中
在网络通信中,套接字一定是成对出现的
- 一端的发送缓冲区对应另一端的接收缓冲区
服务器端
-
其接收端也就是读缓冲区,需要从用户端套接字的写缓冲区读数据
read(cfd,buf,sizeof(buf));
-
然后本地实现大小写功能后,需要
把数据写到用户端
的接收端write(cfd,buf,sizeof(buf));
用户端
-
首先需要把用户的
键盘输入
的内容读到自己定义的用户缓冲区buf
fgets(buf,sizeof(buf),stdin);
-
然后自己的发送端的内容(客户端写的)写到自己定义的buf,以便输出
write(cfd,buf,strlen(buf));
-
最后buf的数据,写出到屏幕上
write(1,buf,sizeof(buf));
查看端口的命令
netstat -apn | grep 具体端口号
把错误处理进行封装
wrap.c(没有主函数)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
void print_err(char* str){perror(str);exit(-1);
}
/*socket封装*/
int Socket(int domain, int type, int protocol){int n=socket(domain,type,protocol);if(n==-1) print_err("socket fails\n");return n;}/*bind封装*/
int Bind(int sockfd, const struct sockaddr *addr,\socklen_t addrlen){int n=bind(sockfd,addr,addrlen);if(n==-1) print_err("bind fails\n");return n;
}
/*listen封装*/
int Listen(int sockfd, int backlog){int n=listen(sockfd,backlog);if(n==-1) print_err("listen fails\n");return n;
}
/*accept封装*/
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){int n=accept(sockfd,addr,addrlen);if(n==-1) print_err("accept fails\n");return n;
}
/*connect封装*/
int Connect(int sockfd, const struct sockaddr *addr,\socklen_t addrlen){int n=connect(sockfd,addr,addrlen);if(n==-1) print_err("connect fails\n");return n;
}
wrap.h(头文件声明)
#ifndef MY_WRAP
#define MY_WRAP
extern void print_err(char* str);
extern int Socket(int domain, int type, int protocol);
extern int Bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
extern int Listen(int sockfd, int backlog);
extern int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
extern int Connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
#endif
server.c(调用大写程序)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#include "wrap.h"
#define PORT 6799
#define IP "127.0.0.1"
int main(){int cfd,sfd;char buf[30];int n;struct sockaddr_in serverAddr;struct sockaddr_in clientAddr;socklen_t cli_addr_len;//socket()sfd=Socket(AF_INET,SOCK_STREAM,0);//bind()bzero(&serverAddr,sizeof(serverAddr));serverAddr.sin_family=AF_INET;serverAddr.sin_port=htons(PORT);inet_pton(sfd,IP,&serverAddr.sin_addr.s_addr);Bind(sfd,(struct sockaddr*)&serverAddr,\sizeof(serverAddr));//listen()Listen(sfd,12);//accept()cli_addr_len=sizeof(clientAddr);cfd=Accept(sfd,(struct sockaddr*)&clientAddr,&cli_addr_len);/*显示一下哪个客户端连接了*/char client_IP[100]={0};printf("client IP=%s,client PORT=%d\n",\inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,\client_IP,sizeof(client_IP)),\ntohs(clientAddr.sin_port));/*转换大小写*/n=read(cfd,buf,sizeof(buf));int i;for(i=0;i<n;i++){buf[i]=toupper(buf[i]);}write(cfd,buf,n);close(sfd);close(cfd);return 0;
}
client.c(调用小写函数)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#include "wrap.h"
#define PORT 6799
#define IP "127.0.0.1"
int main(){int n;char buf[100];int cfd=Socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in server_addr;memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);inet_pton(AF_INET,IP,&server_addr.sin_addr.s_addr);Connect(cfd,(struct sockaddr*)&server_addr,\sizeof(server_addr));//把用户输入读到buffgets(buf,sizeof(buf),stdin);//把buf内容写到用户端write(cfd,buf,strlen(buf));//把返回结果写回屏幕n=read(cfd,buf,sizeof(buf));write(1,buf,n);close(cfd);return 0;
}
程序运行
accept()函数错误处理
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){int n=accept(sockfd,addr,addrlen);/*异常处理和错误都会返回-1,所以要单独分析---比如:一个连接被断开了,错误号:ECONNABORTED---比如:因为收到信号被断开连接,错误号:EINTR这时候要么重新连接,要么结束连接,这里选“重新连接”*/
reconnectted:if(n==-1){if(errno==ECONNABORTED||(errno==EINTR))goto reconnectted;else print_err("accept fails\n");}return n;
}
需要考虑是正常退出(信号主动退出)还是异常退出
read()函数返回值
-
正常情况大于0
—实际读到字节数- 比如buf[1024],read恰好读到那么大,==1024
- 如果不足,读到多少字节就返回多少字节
-
恰好
返回0
----读到了文件末尾,管道被关闭了,socket被关闭如果没被关闭,还没写数据,不会读到0,因为会阻塞,直到读到或被终止
-
等于-1
- 正常退出
- errno==EINTR,被信号终止,退出或重启
- errno==EAGAIN,非阻塞方式读,但是还没有数据的情况
- 。。。
- 异常—(-1,出现错误)
- 正常退出
/*read封装*/
ssize_t Read(int fd, void *buf, size_t count){int n=read(fd,buf,count);if(n==-1){again:if(errno==EINTR||errno==EAGAIN)goto again;else print_err("read fails\n");}return n;
}
TCP协议
由于网络层与硬件连接紧密,所以具有不稳定性(路由器宕机,网络传输慢)
传输层:
- 如果
完全不弥补
,尽力而为选用:无连接不可靠的报文传输—UDP
- 如果
完全弥补
选用:面向连接的可靠数据包传输—TCP
TCP的通信时序图
- 两条竖线表示通讯的两端
- 从上到下表示时间的先后顺序
- 图中的箭头都是斜的—数据从一端传到网络的另一端也需要时间
请求标志 数据包编号(携带数据大小) 序号
这个序号在网络通讯中用作
临时地址
- 每发一个数据字节,这个序号要加1
- 在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况
- 规定
SYN位
和FIN位
也要占一个序号
- 首先客户端主动发起连接、发送
请求
- 然后服务器端
响应
请求- 然后客户端主动
关闭
连接
三次握手----建立连接
miss
表示最大段尺寸
- 如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片
- 为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度
在建立连接的同时,双方协商
了一些信息,例如双方发送序号的初始值
、最大段尺寸
等
数据传输:三次握手后,四次握手前
- 客户端发出段4
从序号1001开始的20个字节数据,发送数据
- 服务器发出段5
- 确认序号为1021,对序号为1001-1020的数据表示确认收到
- 服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据
- 客户端发出段6
对服务器发来的序号为8001-8010的数据表示确认收到
在数据传输过程中,ACK和确认序号
是非常重要的,
- 应用程序交给TCP协议发送的数据会暂存在TCP层的
发送缓冲区
中 - 发出数据包给对方之后,只有
收到对方应答的ACK段
才知道该数据包确实发到了对方 - 如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过
等待超时后
TCP协议自动将发送缓冲区中的数据包重发
如图,不是发送一个数据就会得到一个应答请求
比如发送端发送数据快,接收端可以一下接收三四个数据,再答复ack
只要数据包序号是累加的即可
四次握手----关闭连接
TCP连接是
全双工
的,因此每个方向都必须单独
进行关闭
一个 FIN只能终止
这个方向
的连接,只意味着这一方向
上没有数据流动
链路层的以太网帧大小很小,目的就是封装的包小,方便丢包后重传
如果数据过大,就只能分割数据,获得多个传输的数据包
多进程并发服务器
父进程应答请求,fork子进程通信
//父进程不断通过复制子进程实现多个连接while(1){//accept()----别忘了对第三个参数取地址cfd=Accept(sfd,(struct sockaddr*)&clientAddr,&client_addr_len);//fork()pid=fork();if(pid >0){//父进程close(cfd);//没有请求了,关闭cfd}else if(pid==0){close(sfd);break;//跳出进入子进程交互}}//结束应答if(pid==0){//子进程去交互while(1){//转换大小写n=Read(cfd,buf,sizeof(buf));if(n==0){//读到了末尾close(cfd);return 0;}for(i=0;i<n;i++)buf[i]=toupper(buf[i]);write(cfd,buf,n);}}
运行结果(nc)
僵尸进程问题
子进程结束,但是父进程没有回收
#include <sys/types.h>#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
实现:
void wait_child(int singo){//0---所有子进程无差别回收while(waitpid(0,NULL,WNOHANG)>0);exit(-1);
}
if(pid >0){//父进程close(cfd);//没有请求了,关闭cfdsignal(SIGINT,wait_child);//子进程回收}
打印信息:网络字节序to本机
printf("client ip=%s,port=%d\n",inet_ntop(AF_INET,\&serverAddr.sin_addr.s_addr,client_IP,sizeof(client_IP)),\ntohs(serverAddr.sin_port));
inet_ntop
(AF_INET**,&地址.s_addr,写入的缓存地址,**缓存长度);ntohs
(地址.sin_port);
实现了多进程并发的完整程序
wrap.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
void print_err(char* str){perror(str);exit(-1);
}
/*socket封装*/
int Socket(int domain, int type, int protocol){int n=socket(domain,type,protocol);if(n==-1) print_err("socket fails\n");return n;}/*bind封装*/
int Bind(int sockfd, const struct sockaddr *addr,\socklen_t addrlen){int n=bind(sockfd,addr,addrlen);if(n==-1) print_err("bind fails\n");return n;
}
/*listen封装*/
int Listen(int sockfd, int backlog){int n=listen(sockfd,backlog);if(n==-1) print_err("listen fails\n");return n;
}
/*accept封装*/
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){int n=accept(sockfd,addr,addrlen);/*异常处理和错误都会返回-1,所以要单独分析---比如:一个连接被断开了,错误号:ECONNABORTED---比如:因为收到信号被断开连接,错误号:EINTR这时候要么重新连接,要么结束连接,这里选“重新连接”*/
reconnectted:if(n==-1){if(errno==ECONNABORTED||(errno==EINTR))goto reconnectted;else print_err("accept fails\n");}return n;
}
/*connect封装*/
int Connect(int sockfd, const struct sockaddr *addr,\socklen_t addrlen){int n=connect(sockfd,addr,addrlen);if(n==-1) print_err("connect fails\n");return n;
}
/*read封装*/
ssize_t Read(int fd, void *buf, size_t count){int n=read(fd,buf,count);if(n==-1){again:if(errno==EINTR||errno==EAGAIN)goto again;else print_err("read fails\n");}return n;
}
wrap .o
#ifndef MY_WRAP
#define MY_WRAP
extern void print_err(char* str);
extern int Socket(int domain, int type, int protocol);
extern int Bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
extern int Listen(int sockfd, int backlog);
extern int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
extern int Connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
extern ssize_t Read(int fd, void *buf, size_t count);
#endif
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>
#include "wrap.h"
#include <sys/wait.h>
#include <signal.h>
#define PORT 6767
void wait_child(int singo){//0---所有子进程无差别回收while(waitpid(0,NULL,WNOHANG)>0);exit(-1);
}
int main(){int sfd,cfd;struct sockaddr_in serverAddr;struct sockaddr_in clientAddr;socklen_t client_addr_len;pid_t pid;char buf[100],client_IP[100];int n,i;sfd=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);/*bind()*/bzero(&serverAddr,sizeof(serverAddr));serverAddr.sin_family=AF_INET;serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);//inet_pton(AF_INET,自己的IP,&地址.sin_adds.s_addr)serverAddr.sin_port=htons(PORT);Bind(sfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));Listen(sfd,12);client_addr_len=sizeof(clientAddr);/*利用多进程实现*///父进程不断通过复制子进程实现多个连接while(1){//accept()----别忘了取地址cfd=Accept(sfd,(struct sockaddr*)&clientAddr,&client_addr_len);//打印信息:ip+端口号printf("client ip=%s,port=%d\n",inet_ntop(AF_INET,\&serverAddr.sin_addr.s_addr,client_IP,sizeof(client_IP)),\ntohs(serverAddr.sin_port));//fork()pid=fork();if(pid >0){//父进程close(cfd);//没有请求了,关闭cfdsignal(SIGINT,wait_child);//子进程回收}else if(pid==0){close(sfd);break;//跳出进入子进程交互}}if(pid==0){//子进程去交互while(1){n=Read(cfd,buf,sizeof(buf));if(n==0){//读到了末尾close(cfd);return 0;}for(i=0;i<n;i++)buf[i]=toupper(buf[i]);write(cfd,buf,n);}}return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>
#include "wrap.h"
#define PORT 6767
int main(int argc,char** argv){int cfd;char* ip_ADDR=argv[1];//不可以用char ip_ADDR[100]char buf[100];struct sockaddr_in serverAddr;cfd=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);bzero(&serverAddr,sizeof(serverAddr));serverAddr.sin_family=AF_INET;serverAddr.sin_port=htons(PORT);inet_pton(AF_INET,ip_ADDR,&serverAddr.sin_addr.s_addr);Connect(cfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));/*程序实现*/while(1){fgets(buf,sizeof(buf),stdin);write(cfd,buf,strlen(buf));int n=read(cfd,buf,sizeof(buf));write(1,buf,n);}return 0;
}
#include"wrap .h"的头文件需要放到后面,不然编译不通过
基础补充
典型协议
传输层
常见协议有TCP/UDP
协议
- TCP传输控制协议(Transmission Control Protocol)
是一种
面向连接的
、可靠的
、基于字节流的
传输层通信协议
- UDP用户数据报协议(User Datagram Protocol)
是OSI参考模型中一种
无连接的
传输层协议,提供面向事务
的简单不可靠
信息传送服务
应用层
常见的协议有HTTP
协议,FTP
协议
- HTTP超文本传输协议(Hyper Text Transfer Protocol)
- FTP文件传输协议(File Transfer Protocol)
网络层
常见协议有IP协议
、ICMP协议
、IGMP协议
- IP协议是因特网互联协议(Internet Protocol)
- ICMP协议是Internet
控制报文
协议(Internet Control Message Protocol)是TCP/IP协议族的一个子协议,用于在
IP主机、路由器
之间传递控制消息
- IGMP协议是 Internet
组管理
协议(Internet Group Management Protocol)是因特网协议家族中的一个
组播
协议。该协议运行在主机和组播路由器
之间。
网络接口层
常见协议有ARP协议
、RARP协议
、以太网帧协议
- [ARP]协议是正向地址解析协议(Address Resolution Protocol)
通过已知的IP,寻找对应主机的MAC地址
[RARP]是反向地址转换协议
通过MAC地址确定IP地址
网络应用程序设计模式
C/S模式
- 传统的网络应用设计模式,客户机(client)/服务器(server)模式
- 需要在通讯两端各自部署客户机和服务器来完成数据通信
B/S模式
- 浏览器()/服务器(server)模式
- 只需在一端部署服务器,而另外一端使用每台PC都默认配置的
浏览器
即可完成数据的传输。
优缺点
C/S模式
- 优点
- 将数据缓存至客户端本地,从而提高数据传输效率,可以保证性能
- 所采用的协议相对灵活,可以在标准协议的基础上根据需求裁剪及定制
- 缺点
- 工作量将成倍提升,开发周期较长
- 从用户角度出发,需要将客户端安装至用户主机上,对用户主机的安全性构成威胁
B/S模式
- 没有独立的
客户端
,使用标准浏览器作为客户端,其工作开发量较小 - 移植性非常好,不受平台限制
缺点:
- 协议选择不灵活,必须采用标准协议
- 缓存数据慢,传输数据量受限制
分层模型
OSI七层模型
TCP/IP四层模型
TCP/IP网络协议栈:
应用层(Application),传输层(Transport),网络层(Network)和链路层(Link)四层
目的主机收到数据包后,如何经过各层协议栈最后到达应用程序
以太网驱动程序
根据以太网首部
中的“上层协议”字段确定该数据帧的有效载荷是IP、ARP还是RARP协议的数据报
有效载荷:payload,指除去协议首部之外实际传输的数据
- 假如是
IP数据报
,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP - 假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“
端口号
”字段确定应该将应用层数据交给哪个用户进程
IP地址是标识网络中不同主机的地址
而端口号就是同一台主机上标识不同进程的地址
IP地址和端口号合起来标识网络中唯一的进程。
-
虽然
IP、ARP和RARP
数据报都需要以太网驱动程序
来封装成帧
- 但是从功能上划分,ARP和RARP属于链路层,IP属于网络层
-
虽然
ICMP、IGMP、TCP、UDP
的数据都需要IP协议
来封装成数据报
- 但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。