本案例运行环境:Ubuntu 12.04.1 LTS
1、基本概念
网络的七层模型:
- 物理层
- 数据链路层
- 网络层
- 传输层
- 会话层
- 表示层
- 应用层
其中:1、2、3层主要面向通过网络端到端的数据流, 4、5、6、7层定义了程序的功能
TCP/IP的四层模型
- 应用层:应用层和传输层之间的接口由端口号和套接字所定义,包含Telnet和文件传输协议(FTP)
- 传输层:端到端的数据传输,从一个应用传输到它的远程对等实体,包含UDP,TCP协议
- 网络层:网络上传输的基本信息单位,有IP、ICMP、ARP、RARP等
- 网络接口层:也叫链路层,数据链路层,实际网络硬件接口
TCP协议
传输控制协议,为应用程序提供可靠的通信连接,他具有可靠的、面向连接的、不容易拥塞的、具有流量控制功能的协议。适用于一次传输大批量数据的情况并可要求得到相应的应用程序提供全双工可交付服务,采取许多机制确保端到端结点之间的可靠数据传输,如采用序列号、确认重传和滑动窗口
TCP具有重传功能的积极确认技术
确认:指接收端在正确收到报文后向接收端发一个确认(ACK)信息。发送端将每个已发送的报文备份在自己的缓冲区,在收到相应的确认之前不会丢弃保存的报文段
积极:发送端在每一个报文发送完成后,同时启动一个定时器,若加入定时器的定时期满而关于报文段的确认信息还没有达到,则认为已经丢失并主动重发
三次握手:
第一次握手:客户端将syn(同步)设置为1,同时产生一个随机数seq发送给服务端
第二次握手:服务端接收到报文,将syn=1,产生随机数seq根ack=端口seq+1的确认报文发送给客户端
第三次握手:客户端接受到服务端报文,同时发送一个确认报文ack=服务端seq+1
UDP协议
用户数据报协议,一种无连接协议。它适用于对数据可靠性的要求不那么高,实时性高、网络状况良好的情况下
2、套接字
套接字是操作系统内核中的一个数据结构,他是网络中的结点进行相互通信的门户,是网络进程的ID。 网络通信本质上是进程间通信(不同计算机上的进程间通信) 套接字中包含端口号,而在一个计算机中,一个端口号一次只能分配给一个进程。从而精确到不同计算机上进程间通信
2.1 Socket
概念
它是一种特殊的I/O接口,也是一种文件描述符。每一个Socket用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}
类型
- 流式Socket:用于TCP通信,流式套接字提供可靠的、面向连接的通信流
- 数据报Socket:用于UDP通信,数据报套接字定义了一种无连接的服务,数据通过相互独立的数据报文进行传输
- 原始Socket:用于新的网络协议实现的测试,允许对底层协议IP或ICMP进行直接访问
网络编程中基本信息数据结构
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14字节的地址协议,包含Socket的IP地址和端口号*/
};
struct sockaddr_in
{
short int sin_family; //地址族,在<netinet/in.h>中。AF_INET表示IPv4,AF_INET6表示IPv6
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8]; //填充0以保持与struct sockaddr同样大小
};
struct in_addr
{
unsigned long int s_addr; //32位IPv4地址,网络字节序
}
2.2 数据存储优先顺序转换
同为小端,异为大端: 内低数低,内高数高:小端 内低数高,内高数低:大端 网络字节序都是大端 相互转换的4个函数: htons()、ntohs()、htonl()、ntohl() h:host,主机 n:network,网络 s:short 16位 l:long 32位在网络编程中端口号和IP地址均要进行顺序转化,IP地址通过地址格式转化会自动变为网络字节序,故若IP地址通过地址格式转换了,就只要将端口号进行转换即可
函数详解
表头文件
#include <netinet/in.h>
定义函数
unsigned long int htonl(unsigned long int hostlong);
函数说明
将32位主机字节转换成网络字节
返回值
返回对应的网络字节序
综合案例
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
char port[] = "1000";
struct sockaddr_in sockaddr;
sockaddr.sin_port = htons(atoi(port));
printf("转换前字节序:%x\n",atoi(port));
printf("转换后字节序:%x\n",sockaddr.sin_port);
return 0;
}
运行结果
linux@ubuntu:~/test$ gcc to.c //文件名为to.c
linux@ubuntu:~/test$ ./a.out
转换前字节序:3e8
转换后字节序:e803
2.3 地址格式转换
将点分十进制表示的数值转换成Socket编程时使用的32位网络字节序的二进制值
函数详解
表头文件
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
定义函数
unsigned long int inet_addr(const char* cp);
函数说明
将参数cp所指的网络地址字符串(以数字和点组成的字符串)转换成二进制数字
返回值
成功返回对应的网络二进制的数字,失败返回-1
表头文件
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
定义函数
int inet_aton(const char* cp,struct in_addr *inp);
函数说明
将参数cp所指的网络地址字符串(以数字和点组成的字符串)转换成二进制数字,然后存于参数inp所指的in_addr结构中
返回值
成功返回非0值,失败返回-1
表头文件
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
定义函数
char * inet_ntoa(struct in_addr in);
函数说明
将参数in所指的二进制数字转换成网络地址字符串(以数字和点组成的字符串),然后指向此网络地址字符串的值返回
返回值
成功返回字符串指针,失败返回NULL
综合案例
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
char ip[] = "192.168.0.101";
struct in_addr myaddr; //存储32位IPv4网络字节序的结构体
/*inet_aton 网络地址换成二进制数字*/
int iRet = inet_aton(ip,&myaddr);
printf("%x\n",myaddr.s_addr);
/*inet_addr 网络地址换成二进制数字*/
myaddr.s_addr = inet_addr(ip);
printf("%x\n",myaddr.s_addr);
/*inet_ntoa 二进制数字转换成网络地址*/
myaddr.s_addr = 0xac100ac4;
char* IPv4 = inet_ntoa(myaddr);
printf("%s\n",IPv4);
return 0;
}
运行结果
linux@ubuntu:~/test$ gcc wl01.c //文件名叫wl01.c
linux@ubuntu:~/test$ ./a.out
6500a8c0
6500a8c0
196.10.16.172
3、网络编程
3.1 建立Socket通信
函数详解
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int socket(int domain,int type,int protocol)
函数说明
用来建立一个新的Socket,也就是向系统注册,通知系统建立一个通信端口
domain:指定使用何种地址类型
PF_INET/AF_INET:IPv4网络协议
PF_INET6/AF_INET6:IPv6网络协议
....
type:
SOCK_STREAM:提供双向连续可信赖的数据流,即TCP连接,支持OOB机制,在所有数据传送前必须使用connect()来建立连线状态
SOCK_DGRAM:提供不连续不可信赖的数据包连接
SOCK_SEQPACKET:提供连续可信赖的数据包连接
SOCK_RAW:提供原始网络协议存取
SOCK_RDM:提供可信赖的数据包连接
SOCK_PACKET:提供网络驱动程序的直接通信
protocol:
指定Socket所使用的传输协议编号,通常默认设置为0
返回值
成功返回Socket处理代码,即通信文件指针或叫做通信句柄,失败返回-1
常用实例
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0); //表示建立一个IPv4的地址的TCP连接通信
if(sockfd == -1)
{
perror("error!");
exit(-1);
}
3.2 绑定地址
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
函数说明
用于对Socket进行定位
给参数sockfd的Socket绑定IP地址和端口号等。IP地址端口号等参数my_addr指向一sockaddr结构
*my_addr:sockaddr结构
addrlen:sockaddr的结构长度
返回值
成功返回0,失败返回-1
常用实例
/*******************************************************************
*此为经常使用的用法,并不是一个完整的可以编译的源代码
*绑定地址的步骤为:
*1.定义一个struct sockaddr_in类型的变量
*2.将变量内数据清空
*3.为三个变量sa_family 地址族,sin_port 端口号,sin_addr IP地址赋值
*4.地址绑定
******************************************************************/
struct sockaddr_in my_addr;
memset(&my_addr,0,sizeof(struct sockaddr));
//或 bzero(&my_addr,sizeof(struct sockaddr));
my_addr.sa_family = AF_INET; //采用IPv4网络协议
my_addr.sin_port = htons(atoi("8080")); //表示端口号为8080,通常是一个大于1024的端口值
//htons() 用来将指定的16位hostshort转换成网络字节序,本质上就是将存在主机上的两个字节内数据的数据转化成网络字节顺序
//atoi() 用于将数字字符串转换成int型数字,若是一个整数,则不需要这个函数进行转换
//inet_addr() 用于将点分十进制的IP地址字符串转换成二进制数字,若位INADDR_ANY表示自动填充本机IP地址
my_addr.sin_addr = inet_addr("192.168.1.1");
//sockfd为上一步创建的通信句柄(通信文件指针),参数2为强制类型转换,因为第二个参数的类型是strcut sockaddr,往结构体内填充具体数据又要用struct sockaddr_in这个结构体,故需要强制类型转换,为什么不直接使用struct sockaddr_in这个结构体呢,因为使用IPv4和IPv6所需要的字长是不一样的
if(bind(sockfd,(struct sockaddr *)&myaddr,sizeof(struct sockaddr)) == -1)
{
perror("bind error!");
}
3.3 监听
函数详解
表头文件
#include <sys/socket.h>
定义函数
int listen(int sockfd,int backlog);
函数说明
用于等待连接,用来等待参数sockfd的Socket连线。
backlog:指定同时能处理的最大连接要求
返回值
成功返回0,失败返回-1
注意
listen()只适用于SOCK_STREAM或SOCK_SEQPACKET的Socket类型。若为AF_INET,则参数backlog的最大值可为128
常用实例
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
if(listen(sockfd,10) == -1)
{
perror("listen");
close(sockfd);
exit(-1);
}
3.4 接受请求
函数详解
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int accept(int sockfd,struct sockaddr *addr,int *addrlen);
函数说明
用于接收Socket连线,接收参数sockfd的Socket连线,参数sockfd必须经过bind()和listen()处理过
当有连线进来时,accept()会返回新的Socket处理代码,之后的数据传送于读取就由新的Socket处理,原来参数sockfd的Socket能继续使用accept()来接收新的连线要求,连线成功后,参数sddr所指的结构会被系统填入远程主机的地址数据
addrlen为scokaddr的结构长度
返回值
成功返回新的Socket处理代码,也就是一个新的通信句柄,后续的数据传输和读取都要通过新的这个句柄进行操作,原来的句柄继续使用,继续监听其他客户机的请求,失败返回-1
常用实例
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
struct sockaddr_in clientaddr; //定义一个变量,用于存储远程主机的地址数据
memset(&clientaddr,0,sizeof(struct sockaddr)); //清空结构体
int new_sockfd = accept(sockfd,(struct sockaddr*)&clientaddr,sizeof(struct sockaddr));
if(new_sockfd == -1)
{
perror("accept error!");
close(sockfd);
exit(-1);
}
3.5 连接服务器
函数详解
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int connect(int sockfd,struct sockaddr *serv_addr,int addrlen);
函数说明
用于接收Socket连线,将参数sockfd的Socket连至参数serv_addr指定的网络地址
addrlen:sockaddr的结构长度
返回值
成功返回0,失败返回-1
常用实例
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
struct sockaddr_in seraddr; //定义一个变量,用来存储所需连接的服务器地址信息
memset(&seraddr,0,sizeof(struct sockaddr));
seraddr.family = AF_INET; //IPv4协议
seraddr.sin_port = htons(10000); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.2.19"); //服务器的IP地址
if(connect(sockfd,(struct sockaddr*)&sockaddr_in,sizeof(struct sockaddr)) == -1)
{
perror("connect error!");
close("sockfd");
exit(-1);
}
3.6 发送数据
函数详解
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int send(int s,const void *msg,int len,unsigned int falgs);
函数说明
用于通过Socket传送数据,将数据由指定的Socket传给对方主机。
s:已建立好连接的Socket
msg:欲指向连线的数据内容
len:数据长度
flags:一般设为0,也有其他数值定义,可自行查询
返回值
成功返回实际传送出去的字符数,失败返回-1
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int sendto(int s,const void *msg,int len,unsigned int falgs,const struct sockaddr* to,int tolen);
函数说明
用于通过Socket传送数据,将数据由指定的Socket传给对方主机。
s:已建立好连接的Socket
msg:欲指向连线的数据内容
len:数据长度
flags:一般设为0,也有其他数值定义,可自行查询
to:欲传送的网络地址
tolen:sockaddr的结果长度
返回值
成功返回实际传送出去的字符数,失败返回-1
常用实例
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
char data[256] = "hello C.";
if(send(new_sockfd,data,sizeof(data),0) == -1)
{
perror("send error!");
close(new_sockfd);
close("sockfd");
exit(-1);
}
3.7 接收数据
函数详解
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int recv(int s,void *buf,int len,unsigned int falgs);
函数说明
用于通过Socket接收数据,将接收由指定的Socket传来的数据。
s:已建立好连接的Socket
buf:存储数据的内存空间地址
len:;可接收数据最大长度
flags:一般设为0
MSG_OOB:接收以out-of-band送出的数据
MSG_PEEK:返回来的数据并不会在系统内删除,若再次调用recv()会返回相同的数据内容
MSG_WAITALL:强迫接收到len大小的数据后才能返回,除非有错误发生
MSG_NOSIGNAL:此操作不愿被SIGPIPE信号中断
返回值
成功返回接收到的字符数,失败返回-1
表头文件
#include <sys/types.h>
#include <sys/socket.h>
定义函数
int recvfrom(int s,void *buf,int len,unsigned int falgs,struct sockaddr* from,int *fromlen);
函数说明
用于通过Socket接收数据,将接收由指定的Socket传来的数据。
s:已建立好连接的Socket
buf:存储数据的内存空间地址
len:;可接收数据最大长度
flags:一般设为0
MSG_OOB:接收以out-of-band送出的数据
MSG_PEEK:返回来的数据并不会在系统内删除,若再次调用recv()会返回相同的数据内容
MSG_WAITALL:强迫接收到len大小的数据后才能返回,除非有错误发生
MSG_NOSIGNAL:此操作不愿被SIGPIPE信号中断
from:指定欲传送的网络地址
fromlen:sockaddr的结构长度
返回值
成功返回接收到的字符数,失败返回-1
/*此为经常使用的用法,并不是一个完整的可以编译的源代码*/
char sock_buf[256] = {0};
if(recv(new_sockfd,buf,sizeof(buf),0) == -1)
{
perror("recv error");
close(new_sockfd);
exit(-1);
}
puts(buf);
3.8 简单综合案例
服务器端:server.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int sockfd;
int new_sockfd;
char data[512];
char data_buf[512];
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(struct sockaddr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(10000);
server_addr.sin_addr.s_addr = inet_addr("192.168.124.177");
//建立通信
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("sockfd error.");
exit(-1);
}
//绑定地址
if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr)) < 0)
{
perror("bind error.");
close(sockfd);
exit(-1);
}
printf("服务器启动\n");
//监听
if(listen(sockfd,10) == -1)
{
perror("sockfd error.");
close(sockfd);
exit(-1);
}
new_sockfd = accept(sockfd,NULL,NULL);
if(new_sockfd < 0)
{
perror("accept error.");
close(sockfd);
exit(-1);
}
while(1)
{
if(recv(new_sockfd,data_buf,sizeof(data_buf),0) < 0)
{
perror("recv error.");
close(sockfd);
exit(-1);
}
printf("%s\n",data_buf);
sprintf(data,"get data:%s\n",data_buf);
if(send(new_sockfd,data,sizeof(data),0) < 0)
{
perror("send error.");
close(sockfd);
exit(-1);
}
}
return 0;
}
客户端:server.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int sockfd;
char data[512];
char data_buf[512];
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(struct sockaddr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(10000);
server_addr.sin_addr.s_addr = inet_addr("192.168.124.177");
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("sockfd error.");
exit(-1);
}
printf("启动成功\n");
if(connect(sockfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr)) < 0)
{
perror("connect error.");
close(sockfd);
exit(-1);
}
printf("连接成功\n");
while(1){
printf("输入你想要发送的数据:");
scanf("%s",data);
getchar();
if(send(sockfd,data,sizeof(data),0) < 0)
{
perror("send error.");
close(sockfd);
exit(-1);
}
if(recv(sockfd,data_buf,sizeof(data_buf),0) < 0)
{
perror("recv error.");
close(sockfd);
exit(-1);
}
printf("%s",data_buf);
}
return 0;
}
编译代码
linux@ubuntu:~/test$ gcc server.c -o server -Wall
linux@ubuntu:~/test$ gcc client.c -o client -Wall
运行结果
/*服务器端*/
linux@ubuntu:~/test$ ./server
服务器启动
nihao
dajiahao
shijiediyi
shijiediyi
shijiediyi
/*客户端*/
linux@ubuntu:~/test$ ./client
启动成功
连接成功
输入你想要发送的数据:nihao
get data:nihao
输入你想要发送的数据:dajiahao
get data:dajiahao
输入你想要发送的数据:shijiediyi
get data:shijiediyi
输入你想要发送的数据:^C