- 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");
/*执行程序*/
//把用户输入读到buf
char 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地址+端口号
,才能建立网络连接
服务器端
-
其接收端也就是读缓冲区,需要从用户端套接字的写缓冲区读数据
-
然后本地实现大小写功能后,需要
把数据写到用户端
的接收端
用户端
-
首先需要把用户的
键盘输入
的内容读到自己定义的用户缓冲区buf
-
然后自己的发送端的内容(客户端写的)写到自己定义的buf,以便输出
-
最后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));
//把用户输入读到buf
fgets(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被关闭 -
等于-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协议
TCP的通信时序图
- 两条竖线表示通讯的两端
- 从上到下表示时间的先后顺序
- 图中的箭头都是斜的—数据从一端传到网络的另一端也需要时间
- 首先客户端主动发起连接、发送
请求
- 然后服务器端
响应
请求 - 然后客户端主动
关闭
连接
三次握手----建立连接
miss
表示最大段尺寸
在建立连接的同时,双方协商
了一些信息,例如双方发送序号的初始值
、最大段尺寸
等
数据传输:三次握手后,四次握手前
- 客户端发出段4
- 服务器发出段5
- 客户端发出段6
在数据传输过程中,ACK和确认序号
是非常重要的,
- 应用程序交给TCP协议发送的数据会暂存在TCP层的
发送缓冲区
中 - 发出数据包给对方之后,只有
收到对方应答的ACK段
才知道该数据包确实发到了对方 - 如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过
等待超时后
TCP协议自动将发送缓冲区中的数据包重发
四次握手----关闭连接
多进程并发服务器
父进程应答请求,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);//没有请求了,关闭cfd
signal(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);//没有请求了,关闭cfd
signal(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;
}
基础补充
典型协议
传输层
常见协议有TCP/UDP
协议
- UDP用户数据报协议(User Datagram Protocol)
应用层
常见的协议有HTTP
协议,FTP
协议
网络层
常见协议有IP协议
、ICMP协议
、IGMP协议
- IGMP协议是 Internet
组管理
协议(Internet Group Management Protocol)
网络接口层
常见协议有ARP协议
、RARP协议
、以太网帧协议
-
[RARP]是反向地址转换协议
网络应用程序设计模式
C/S模式
- 传统的网络应用设计模式,客户机(client)/服务器(server)模式
- 需要在通讯两端各自部署客户机和服务器来完成数据通信
B/S模式
- 浏览器()/服务器(server)模式
- 只需在一端部署服务器,而另外一端使用每台PC都默认配置的
浏览器
即可完成数据的传输。
优缺点
C/S模式
- 优点
- 将数据缓存至客户端本地,从而提高数据传输效率,可以保证性能
- 所采用的协议相对灵活,可以在标准协议的基础上根据需求裁剪及定制
- 缺点
- 工作量将成倍提升,开发周期较长
- 从用户角度出发,需要将客户端安装至用户主机上,对用户主机的安全性构成威胁
B/S模式
- 没有独立的
客户端
,使用标准浏览器作为客户端,其工作开发量较小 - 移植性非常好,不受平台限制
缺点:
- 协议选择不灵活,必须采用标准协议
- 缓存数据慢,传输数据量受限制
分层模型
OSI七层模型
TCP/IP四层模型
目的主机收到数据包后,如何经过各层协议栈最后到达应用程序
以太网驱动程序
根据以太网首部
中的“上层协议”字段确定该数据帧的有效载荷是IP、ARP还是RARP协议的数据报
- 假如是
IP数据报
,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP - 假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“
端口号
”字段确定应该将应用层数据交给哪个用户进程
-
虽然
IP、ARP和RARP
数据报都需要以太网驱动程序
来封装成帧
- 但是从功能上划分,ARP和RARP属于链路层,IP属于网络层
-
虽然
ICMP、IGMP、TCP、UDP
的数据都需要IP协议
来封装成数据报
- 但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。