###1.网络体系结构的认识
-
协议
控制网络中数据的传送和接收
。定义通信实体交换报文的格式和次序
以及报文传输或接收及其他事件所采取的动作。 -
网络的分层结构
-
各层协议
- 应用层:Teinet ,FTP,HTTP,DNS ,SNMP 和SMTP等
- 传输层:TCP和UDP
- 网络层: IP,ICMP和IGMP
- 链路层:以太网,令牌环网,FDDI等
-
各层功能:
-
应用层:向使用网络的用户提供特定的常用的应用程序。
- 表示层:通信用户之间数据格式的转换,数据压缩及加解密等
- 会话层:对数据传输进行管理,包括数据交换的定界,同步,建立检查点。
-
传输层:进程之间彼此发送报文,无需考虑具体物理链路。
-
网络层:把原主机上的分组发送到任何一台联网主机上。
-
链路层:把接收到的网络数据报通过该层的物理接口发送到传输介质上,或从物理网络上接收数据帧,抽出IP数据报并交给IP层。
-
-
总的来说,
网络体系结构
由网络层次结构及其每层所使用的协议所组成。
###2. TCP/IP简述
由上面网络分层结构可以看出,TCP协议和UDP协议是传输层协议,其中TCP是面向连接
的流传输层协议,只有保证服务器端和客户端的三次"握手"后,两端才能进行安全的数据传输。UDP是无连接
的传输协议,不对数据报进行检查和修改,无需等待对方的应答,不是比较安全的传输协议,但比tcp的网络开销小。由于考虑到安全,可靠,所以TCP协议是比较常用的传输协议。
###3.关于三次"握手"和四次"挥手"
- 为什么会有三次"握手"和四次"挥手"?
记得在csdn里面看过与此相关的一个故事,现在原文章找不到了,我简述一下吧!便于理解这个过程。
说隔壁老王媳妇让老王上街买醋,半道上他遇到了貌似是多年不见得好朋友老李,于是他向老李问候,说:“老李?你好?”表示客户端向服务器发送第一次请求(第一次握手)
,这时老李听到有人向他问好,声音貌似是好友老王(注意此时老李并不100%确定确定是老王叫他),于是疑惑的回了一句"老王?你好?"(服务器向客户端发回应(第二次握手)
),当老王听到老李的回应,确定是好友老李(可是老李那边还不能确定叫他的是老王),所以老王肯定得再回应老李,于是便又回复了老李一句:“我就知道是你,找到女朋友没?”(客户端在此向服务器发回复(第三次握手)
),老李听到这话,终于确定是老王了,可他听到老王这"扎心"一问,气不打一处来,于是(我这故事编不下去了)
经过"三次握手",TCP传输协议的安全性可见一斑。
至于四次"挥手",这样理解,老王和老李叙旧叙了一天后,老李话非常多,老王想要回家了,给老李说要回家,老李对老王也别感兴趣,听了老王的请求(客户端向服务器发送断开请求
),但是老李话说了半截还没给老王把一些事情讲完(当服务器端向客户端传送数据时
),所以将老王的请求先搁置了下来,老李加快语速将当前话讲完后(数据发完了
),又回到老王的请求,回复老王说"好的"(服务器回复断开连接请求
)(两次挥手完毕),之后的两次挥手是两端对于断开做了又一次确认。 - TCP如何进行连接“三次握手”和断开连接四次“挥手" ?
下面推荐的文章比较通俗易懂的介绍了详细过程
https://blog.csdn.net/liushall/article/details/81697831
###3.socket套接字
- 刚开始学的时候,也没有深究这个函数的作用,只知道他能产生一个套接字,但是网上看了一篇文章,说网络编程一切皆socket,细想一下,确实在理。所谓套接字,就相当于网络应用编程的接口。一切客户端与服务器之间的交互都是通过套接字实现的。
- 在linux操作系统中,一切皆文件,所以socket函数产生的套接字本质上还是文件描述符,只不多描述符所指文件是一个网络连接文件(linux系统中还有像管道文件,FIFO文件,终端文件…),他可通过调用send和recv实现网络数据传输和获取。
- 今天所说的TCP/IP在通过socket产生套接字时,要设置为流格式。即sock_fd = socket(AF_INET ,SOCK_STREAM);
- 有了套接字,那么依据什么实现给指定的处于网络中的目标发送数据。通过已知端口的服务器。多客户端通过连接相同端口的服务器,进行数据的传输。
- 那么自然就有了存服务器端口和地址的结构
struct sockaddr{
unsigned short sa_family;//地址家族 如常用的AF_INET
char sa_data[14] ;//14字节协议地址
//存储了目标套接字的端口和地址信息,不是很明智,
//所以以下就有了另一个结构来处理
};
struct sockaddr_in{
short int sin_family ;//通信地址
unsigned short int sin_port ;//端口
struct in_addr sin_addr ;//地址
unsigned char sin_zero[8] ;//填充字节,一般设置为0
}
struct in_addr{
unsigned long s_addr ;//表示一个32位的IPv4地址
}
- 作为服务器要设置地址信息
struct sockaddr_in sock ;
sock.sin_family = AF_INET ;
//端口号一般设置为1024到65535之间
sock.sin_port = htons(7230);
//INADDR_ANY表示本服务器将处理所有网络连接请求
sock.sin_addr.s_addr = htol(INADDR_ANY);
服务器地址和端口设置好了,接下来需要服务器端的的套接字绑定调用
int bind(int sock_fd , struct sockaddr * my_addr ,socklen_t addrlen);
使其套接子和一定端口关联起来,然后就调用监听各端口是否有外部链接
int listen(int sock_fd , struct sockaddr* my_addr , socklen_t addrlen);
accept();参数和bind参数是一样的,当给连接分唯一的套接字以后传输数据都通过分配的这个套接字和对应连接进行数据传输。
以上都是服务器端在和客户端连接之前要做的工作。
客户端这边流程比较简单就创建个套接字,然后只需要调用设置要连接的服务器端口,调用connect()函数与服务器连接。
###基于epoll下的服务器和客户端实现
server端
#include<stdio.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/fcntl.h>
#define MAX 20
#define PORT 8000
typedef struct data{
int i ;
int cmd ;
}Data;
int epoll_fd;
int init_sock(int sock_fd);
void user_process(void* ret);
void setnonblocking(int sock_fd);
//将套接字设置为非阻塞模式
void setnonblocking(int sock_fd){
int opts ;
opts = fcntl(sock_fd ,F_GETFL) ;
if(opts < 0){
printf("F_GETFL") ;
return ;
}
opts = (opts|O_NONBLOCK);
if(fcntl(sock_fd , F_SETFL ,opts) < 0){
printf("F_SETFL");
return ;
}
}
//初始化服务器端套接字
int init_sock(int sock_fd){
int optval ;
optval = 1 ;
sock_fd = socket(AF_INET , SOCK_STREAM , 0);
//保证套接字断开连接后能重新绑定端口
if(setsockopt(sock_fd , SOL_SOCKET ,SO_REUSEADDR ,(void *)&optval ,sizeof(int)) < 0){
printf("setsockopt error!\n");
}
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET ;//14字节序地址
serv_addr.sin_port = htons(PORT);//端口号
//INADDR_ANY一个服务器上的所有网卡,多个本地IP地址都进行绑定端口号,进行倾听
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock_fd ,(struct sockaddr*)(&serv_addr) , sizeof(serv_addr))< 0){//绑定端口
printf("bind") ;
return 0 ;
}
printf("bind finish...... ;)\n");
if(listen(sock_fd , MAX)< 0){//监听端口
printf("listen");
return 0 ;
}
printf("Listening..... :)\n");
return sock_fd;
}
int main(){
int sock_fd ;
int i ,ret;
int conn_fd ;
sock_fd = init_sock(sock_fd);
if(sock_fd == 0){
return 0 ;
}
epoll_fd = epoll_create(256);
//定义epoll事件结构体
struct epoll_event ep_ev ;
//将事件设置为可读事件
ep_ev.events = EPOLLIN ;
//将套接字设置为epoll类型套接字
ep_ev.data.fd = sock_fd ;
//将套接字注册到事件列表中
ret = epoll_ctl(epoll_fd , EPOLL_CTL_ADD , sock_fd ,&ep_ev) ;
if(ret < 0){
printf("epoll_ctl\n");
return 0 ;
}
//定义epoll表
struct epoll_event evs[MAX] ;
while(1){
//等待有活跃事件将其加入到就绪列表中
int epoll_ret = epoll_wait(epoll_fd , evs , MAX , -1);
if(epoll_ret > 0){
//轮询就绪事件
for( i = 0 ; i < epoll_ret ;i++ ){
//如果就绪事件时新连接且为可读事件
if(evs[i].data.fd == sock_fd&&(evs[i].events&EPOLLIN)){
struct sockaddr_in cli_addr ;
socklen_t cli_len ;
cli_len = sizeof(struct sockaddr_in);
//进行套接字连接,产生新套接字
conn_fd = accept(sock_fd , (struct sockaddr*)&cli_addr ,&cli_len) ;
printf("有新连接...... ;)\n");
if(conn_fd < 0){
printf("连接出错!\n") ;
return 0;
}
//设置为非阻塞模式,像本程序种单纯的打印数据,就将非阻塞断了,否则客户端会循环打印数据
// setnonblocking(conn_fd);
//设置事件为可读事件且为边缘触发模式ET
ep_ev.events = EPOLLIN | EPOLLONESHOT ;
ep_ev.data.fd = conn_fd ;
ret = epoll_ctl(epoll_fd , EPOLL_CTL_ADD ,conn_fd , &ep_ev) ;
if(ret < 0){
printf("注册出错!\n");
exit(0) ;
}
}
else{
if(evs[i].events & EPOLLIN){
pthread_t thid ;
pthread_create(&thid , NULL , (void*)&user_process , (void*)&evs[i].data.fd) ;
}
}
}
}
else{
printf("轮询出错!\n");
return 0;
}
}
close(sock_fd);
}
//模拟处理客户端请求
void user_process(void*ret){
int fd ,temp;
fd = *((int *)ret) ;
Data s ;
while(1){ //接收客户端数据
temp = recv(fd ,(void*)&s , sizeof(s) , 0);
if(temp <= 0){
printf("连接%d已经断开...... +_+\n",fd);
close(fd);
pthread_exit(NULL) ;
}
if(s.cmd == 1){
printf("------------------------------\n");
printf("客户端传来的数字:");
printf("%d\n\n",s.i);
printf("------------------------------\n");
printf("给客户端返回个数据吧?\n");
printf("请输入数据:\n");
scanf("%d",&s.i);
send(fd ,(void *)&s ,sizeof(s) , 0 );
}
else{
s.i = 0 ;
printf("客户端发来的命令不是1\n");
send(fd , (void *)&s , sizeof(s) , 0);
}
}
}
client端
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/stat.h>
#include<string.h>
#define PORT 8000
#define IP "127.0.0.1"
typedef struct data{
int i ;
int cmd ;
}Data ;
void *recieve(void* args);
int main(){
Data s ;
int sock_fd ,choose;
struct sockaddr_in sin ;
memset(&sin , 0, sizeof(sin));
//14字节位网址
sin.sin_family = AF_INET ;
//服务器端端口号,将其端口号的本地字节序转网络字节序
sin.sin_port = htons(PORT);
//将地址转化网络字节序
inet_aton(IP , &sin.sin_addr);
if((sock_fd = socket(AF_INET , SOCK_STREAM , 0))< 0){
printf("socket\n");
return 0;
}
if(connect(sock_fd , (struct sockaddr*)&sin , sizeof(sin)) < 0){
printf("connect error!\n");
return 0 ;
}
pthread_t thid ;
//开线程接收数据
pthread_create(&thid , NULL , (void *)recieve,(void *)&sock_fd) ;
//该循环为主线程,客户端通过主线程给服务器发送请求
while(1){
printf("-------------------------------\n");
printf("请输入要发给服务器的请求(输入1):\n");
scanf("%d" , &(s.cmd));
if(s.cmd == -1 ){
close(sock_fd);
exit(0);
}
printf("请输入一些数据:\n");
scanf("%d",&(s.i));
send(sock_fd , (void*)&s , sizeof(s) , 0);
}
}
//接收服务器端返回的信息
void* recieve(void* args){
Data s ;
int ret,p ;
p = *(int*)args ;
while(1){
if((ret = (recv(p ,(void *)&s , sizeof(s),0)))< 0){
printf("recv\n");
pthread_exit(NULL);
}
else{
printf("从服务器接收到数据:");
printf("%d\n",s.i);
printf("\n");
}
}
}
运行截图
- 该程序实现了客户端和服务器之间简单的数据交互,很多功能可以扩展,本文只写了基本框架。想了解更多服务器和客户端的搭建可以转入以下链接有基本框架导图及epoll的使用流程导图: