网络编程
OSI模型与TCP/IP协议体系结构
OSI模型
七层
理想化模型(并不是真正使用的模型,有参考价值)
应用层 数据处理
表示层 数据的加密解密
ASCII -> 电信号 -> ASCII
明文 -> 加密 -> 电信号 -> 解密 -> 数据
会话层 建立两个进程之间的联系
传输层 保证数据的通讯, 纠错
网络层 主机到主机的路径选择
数据链路层 数据分片成数据帧,数据帧->数据
物理层 数据->比特流
TCP/IP协议
四层:第四层也可以分为两层,网络接口层和物理层
现行的互联网工业标准
应用层 数据处理/封装解析/加密解密
传输层 保证端(进程)到端的数据通讯
网络层 保证主机到主机的数据通讯
网络接口和物理层 屏蔽底层细节
TCP/IP协议族
TCP (Transport Control Protocol) 传输控制协议
IP (Internetworking Protocol) 网间协议
UDP (User Datagram Protocol) 用户数据报协议
ICMP (Internet Control Message Protocol) 互联网控制信息协议
SMTP (Simple Mail Transfer Protocol) 简单邮件传输协议
SNMP (Simple Network manage Protocol)简单网络管理协议
HTTP (Hypertext Transfer Protocol) 超文本传输协议
FTP (File Transfer Protocol) 文件传输协议
ARP (Address Resolution Protocol) 地址解析协议
其中TCP、UDP、IP传输层比较重要,HTTP应用层比较重要
协议通讯模型
数据的封装和传递过程
TCP/IP结构
TCP/IP协议下的数据包
TCP和UDP的区别
共同点
同为传输层协议
不同点
TCP:有连接,可靠,数据不丢失
UDP:无连接,不保证可靠,数据可能会丢失,但在char类型下不容易丢失数据
TCP协议特点
TCP (即传输控制协议) : 是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)
适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP协议特点
**UDP( User Datagram Protocol )**用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
1.发送小尺寸数据(如对DNS服务器进行IP地址查询时)
2.在接收到数据,给出应答较困难的网络中使用UDP。(如: 无线网络)
3.适合于广播/组播式通信中。
4.MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
5.流媒体、VOD、 VoIP、 IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
C/S架构
socket套接字编程
定义
Socket
是一个编程接口
是一种特殊的文件描述符(everything in Unix is a file)
并不仅限于TCP/IP协议
面向连接(Transmission Control Protocol - TCP/IP)
无连接(User Datagram Protocol -UDP和Inter-network Packet Exchange - IPX)
socket类型
**流式套接字(S0CK_ **
TREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_ DGRAM)
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重
复,顺序发送,可能乱序接收。
原始套接字(S0CK_ RAW)
可以对较低层次协议如IP、ICMP直接访问。
IP地址
IP地址是internet中主机的标识,IP地址为32位(IPv4)或者128位(IPv6)。
表示形式:常用点分形式,如192.168.0.1,最后都会转换为一个32位的无符号整数。
IP地址的转换
#include <arpa/inet.h>
inet_aton()
//将strptr所指的字符串转换为32位的网络字节序二进制值
//int inet_aton(const char *strptr,struct in_addr *addrptr);
inet_addr()
//功能同上,返回转换后的地址
//in_addr_t inet_addr(const char *strptr);
inet_ntoa()
//将32位网络字节序二进制转换为点分十二进制的字符串
//char *inet_ntoa(struct in_addr inaddr);
端口号
为了区分一台主机接收到的数据包应该交个哪任务来处理,使用端口号来区别
TCP端口号和UDP端口号独立
端口号一般由IANA管理:
1、众所周知端口:1-1023(1-255之间为众所周知端口,256-1023通常有UNIX系统占用)
2、已登记端口:1024-49151
3、动态或私有端口:49152-65535
字节序
定义:不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO)
小端序:低序字节存储在低地址
将低字节存储在起始地址,称为“little-endian”字节序,Intel、AMD等采用的是这种方式;
大端序:高序字节存储在低地址
将高序字节存储在起始地址,称为”Big-Endian”字节序,由Motorola等所采用;
ARM大小端可选
网络中传输的数据必须按网络字节序,即网络中采用的大端字节序
字节序转换函数
把给定系统所采用的字节序称为主机字节序。为了避免不同类别主机之间在数据交换由于对于字节序的不同而导致的差错,引入了网络字节序。
//主机字节序到网络字节序
u_long htonl (u_long hostlong);
u_short htons (u_short short);
//网络字节序到主机字节序
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);
TCP服务器和客户端的搭建
网络编程相关API
常用函数
socket()//创建套接字
bind()//绑定本机地址和端口
connect()//建立连接
listen()//设置监听套接字
accept()//接收TCP连接
recv(),read(),recvfrom()//数据接收
send(),write(),sendto()//数据发送
close(),shutdown()//关闭套接字
socket
int socket (int domain,int type,int protocol);
domain是地址族
PF_INET // internet协议
PF_UNIX // umix internal协议
PF_NS // Xerox NS协议
PF_IMPLINK// Interface Mes sage协议
type //套接字类型
SOCK_STREAM //流式套接字
SOCK_DGRAM //数据报套接字
SOCK_RAW //原始套接字
protocol //参数通常置为0
eg: int socket fd = socket(AF_INET,SOCK_STREAM,0);
地址相关的数据结构
//通用地址结构
struct socket
{
u_short sa_family; //地址族,AF_XXX
char sa_data[14]; //14字节协议地址
};
//Internet协议地址结构
struct sockaddr_in
{
u_short sin_family; //地址族,AF_INET,2bytes
u_short sin_port; //端口,2bytes
struct in_addr sin_addr;//IPV4地址,4bytes
char sin_zero[8]; //8bytes unused,作为填充
};
//IPv4地址结构
//internet address
struct in_addr
{
in_addr_t s_addr; //u32 network address
}
bind()
#include <sys/types.h>
#include <sys/socket.h>
/*
*sockfd:socket调用返回的文件描述符
*addrlen:sockaddr地址结构的长度
*my_addr:通用地址结构,一般用internet协议地址结构
*返回值:0或-1
*/
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
eg:int status = bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
地址结构的一般用法
1、定义一个struct sockaddr_in类型的变量并清空
struct sockaddr_in myaddr;
memset (&myaddr,0,sizeof(myaddr));
2、填充地址信息
myaddr.sin_family = AF_TNET;
myaddr.sin_port = htons(8888);
myaddr.sin_addr.s_addr = inet_addr("192.168.1.1");
3、将改变量强制转换为struct sockaddr类型在函数中使用
bind(listenfd,(struct sockaddr*)(&myaddr),sizeof(myaddr));
listen
int listen (int sockfd, int backlog);
/*
*sockfd:监听连接的套接字
*backlog:
指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求。
DoS (拒绝服务)攻击即利用了这个原理,非法的连接占用了全部的连接数,造成正常的
连接请求被拒绝。
*返回值: 0或-1
*/
完成listen()调用后,socket变成了监听socket(listening socket)
accept
#include <sys/types.h>
#include <sys/socket.h>
/*
*sockfd:监听套接字
*addr:对方地址
*addrlen:地址长度
*返回值:已建立好的套接字或-1
*/
int accept(int sockfd,struct sockaddr* addr,socklen_t *addrlen);
connect
#include <sys/types.h>
#include <sys/socket.h>
/*
*sockfd:监听套接字
*serv_addr:服务器端的地址信息
*addrlen:地址长度
*返回值:已建立好的套接字或-1
*/
int connect(int sockfd,struct sockaddr*serv_addr,int addrlen);
send和recv
//发送
size_t send(int sockfd,const void *buffer,size_t length,int flags);
//接收
size_t recv(int sockfd,const void *buffer,size_t length,int flags);
read和write
//发送
size_t write(int fd,const void *buf,size_t count);
//接收
size_t read(int fd,void *buf,size_t count);
shutdown
//关闭套接字
howto = 0;
//关闭读通道,但是可以继续往套接字写数据
howto = 1;
//关闭写通道,只能从套接字读取数据
howto = 2;
//关闭读写通道,和close()一样
int shutdown(itn sockfd,int howto);
TCP服务器和TCP客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
//服务器
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0){
perror("socket");
return -1;
}
int opt =1;
int ret = setsocketopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(ret < 0){
perror("setsocket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret < 0){
perror("bind");
return -1;
}
int listenfd = listen(sockfd,10);
if(listenfd < 0){
perror("listen");
return -1;
}
int connfd = accept(listenfd,NULL,NULL);
if(connfd < 0){
perror("accpet");
return -1;
}
send(connfd,"hello",sizeof("hello"),0);
close(connfd);
close(listenfd);
}
//客户端:
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0){
perror("socket");
return -1;
}
int opt =1;
int ret = setsocketopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(ret < 0){
perror("setsocket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_addr.s_addr = inet_addr("192.168.0.7");
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
int connfd = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret < 0){
perror("connect");
return -1;
}
char buf[512] = {0};
recv(connfd,buf,512,0);
printf("%s\n",buf);
close(connfd);
}
UDP发送端和UDP接收端
sendto和recvfrom
size_t sendto(int socket,void *message,size_t length,int flags,struct sockaddr *dest_addr,socklen_t dest_len);
size_t recvfrom(itn socket,void *buffer,size_t length,int flags.struct sockaddr *address,socklen_t *address_len);
代码
发送端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int udpServerInit(char*IP, int port)
{
int ret = 0;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
int opt = 1;
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if(ret < 0)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(IP);
ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret < 0)
{
perror("bind");
return -1;
}
return sockfd;
}
int main()
{
int sockfd = udpServerInit("0.0.0.0", 8080);
if(sockfd < 0)
return -1;
char buf[128] = {0};
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&caddr, &addrlen);
if(ret < 0)
{
perror("recvfrom");
return -1;
}
sendto(sockfd, buf, ret, 0, (struct sockaddr*)&caddr, addrlen);
return 0;
}
接收端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8080);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sendto(sockfd, "hello", sizeof("hello"), 0, (struct sockaddr*)&saddr, sizeof(saddr));
char buf[128] = {0};
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("%s\n", buf);
return 0;
}
IO模型
实质:IO处理的方式
阻塞型IO
最常用、最简单、效率最低
非阻塞型IO
可防止进程阻塞在I/O操作上,需要轮询
IO多路复用
允许同时对多个I/O进行控制
信号
一种异步通信模型
服务器模型
循环服务器:在同一时刻只能响应一个客户端的请求
并发服务器:在同一时刻可以响应多个客户端的请求
TCP并发服务器
1、使用多进程/多线程实现并发
在客户端连入后,创建子进程/子线程处理事务,父进程继续等待链接
#include "server.h"
int main()
{
#if 0
//多进程并发服务
int listenfd = tcpSockInit("0.0.0.0",4520,20);
signal(SIGCHLD,waitson);
while(1){
int connfd = tcpAccept(listenfd);
if(fork() == 0){
close(listenfd);
while(1){
int ret = tidJuge(connfd);
if(ret == 0){
close(connfd);
printf("客户端%d退出链接\n",connfd-3);
exit(0);
}}
}
close (connfd);
}
#else
//多线程并发服务
int listenfd = tcpSockInit("0.0.0.0",4520,20);
while(1){
int connfd = tcpAccept(listenfd);
pthread_t tid = 0;
pthread_create(&tid,NULL,pidJuge,connfd);
pthread_detach(tid);
}
#endif
}
2、使用IO多路复用实现并发
使用select/poll/epoll
select并发服务器
int select (int n,fa_set *read_fds,fd_set *write_fds,fd_set *except_fd,struct timeval *timeout);
/*
* maxfd:所有监控的文件描述符中最大的那一个加一
* read_fds:所有要读的文件描述符的集合
* write_fds:所有要写的文件描述符的集合
* except_fds:其他要向我们通知的文件描述符
* timeout:超时设置
* NULL:一直阻塞,直到有文件描述符就绪或出错
* 时间值为0:仅仅检测文件描述符集的状态,然后立即返回
* 时间值不为0:在指定的时间内,如果没有事件发生,则超时返回
*/
//设置文件描述符所使用的宏
FD_SET 将fd加入到fdset void FD_SET(int fd,fd_set *fdset)
FD_CLR 将fd从fdset里面清除 void FD_CLR(int fd,d_set *fdset)
FD_ZERO 从fdset中清除所有的文件描述符 void FD_ZERO(fd_set *fdset)
FD_ISSET 判断fd是否在fdset集合中 void FD_ISSET(int fd,d_set *fdset)
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include "server.h"
int main()
{
int ret = 0;
int listenfd = TcpServerInit("0.0.0.0", 8080, 20);
if(listenfd < 0)
{
return -1;
}
int maxfd = listenfd;
fd_set rfds;//读事件
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
fd_set tmpfds = rfds;
while(1)
{
rfds = tmpfds;
int n = select(maxfd + 1, &rfds, NULL, NULL, NULL);
if(n < 0)
{
perror("select");
return -1;
}
for(int i = 3; i < maxfd + 1; i++)
{
if(FD_ISSET(i, &rfds))
{
if(i == listenfd)
{
int connfd = acceptClient(i);
if(connfd < 0)
{
return -1;
}
FD_SET(connfd, &tmpfds);
maxfd = maxfd > connfd ? maxfd : connfd;
n--;
}
else
{
char buf[1024] = {0};
ret = read(i, buf, sizeof(buf));
if(ret < 0)
{
perror("read");
return -1;
}
else if(ret == 0)
{
close(i);
FD_CLR(i, &tmpfds);
}
for(int j = 4; j < maxfd + 1; j++)
{
if(FD_ISSET(j, &tmpfds) && j != i)
{
write(j, buf, ret);
}
}
}
}
}
}
return 0;
}
setsockopt设置套接字属性
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t *optlen)
//level取值有三个:
SOL_SOCKET:通用套接字选项
IPPROTO_IP:IP选项
IPPROTO_TCP:TCP选项
超时检测
1、设置套接字超时
struct timval tv;
tv.tv_sec = 5;//设置5秒时间
tv.tv_usec = 0;
setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));//设置接收超时
recv()/recvfrom()
2、使用select完成超时检测
struct fd_set rdfs;
struct timeval tv = {5,0};//设置5秒时间
FD_ZERO(&rdfs);
FD_SET(sockfd,&rdfs);
if(select(sockfd+1,&rfds,NULL,NULL,&tv) > 0)//socket就绪
{
recv()/recvfrom()
}
3、使用信号完成超时检测
void handler(int signo){
return ;
}
struct sigaction act;
sigaction(SIGALRM,NULL,&act);
act.sa_handler = handler;
act.sa_flags &=~SA_RESTART;
sigaction(SIGALRM,&act,NULL);
alarm(5);
if(recv(...) < 0) ...
广播/组播
广播
一对多的通信,使用UDP
1、创建用户数据报套接字
2、设置广播功能开启
3、发送给主机号为255,端口号特定的应用
4、发送数据包
**接收方 client.c **
1、创建用户数据报套接字
2、绑定IP地址(0.0.0.0)绑特定端口号
3、等待接收数据
组播
组播地址为D类地址
1、创建用户数据报套接字
2、接受放地址指定为组播地址
3、指定端口信息
4、发送数据包
接受方 client.c
1、创建用户数据报套接字
2、加入多播组
3、绑定的那个IP地址(0.0.0.0)和端口
4、等待接收数据
Unix域套接字(本地进程间通讯)
数据库的使用
常用命令
在sqlite3中
.tables;//显示数据库中所有表名
.schema <tablename>;//查看表的结构
create table <tablename> (f1 type1,f2 type2,f3 type3...);//创建新表
drop table <tablename>;//删除表
select * from <tablename>;//查询表中所有记录
select * from <tablename> where <expression>;//查询特定条件下的数据
insert into <tablename> values (value1,value2,...);//向表中添加新纪录
delete from <tablename> where <expression>;//删除表中记录
update <tablename> set <f1=value1>,<f2=value2>... where <expression>//更新表中记录
sqlite编程接口
int sqlite3_open(char *path,sqlite3 **db);
/*打开sqlite数据库
*path:数据库文件路径
*db:指向sqlite句柄的指针
*返回值:成功返回0,失败返回错误码(非零值)
*/
int sqlite3_close(sqlite3 *db);
/*关闭sqlite数据库
*返回值:成功返回0,失败返回错误码
*/
const char *sqlite3_errmsg(sqlite3 *db);
//返回错误信息
打开数据库文件
#include <stdio.h>
#include <stdlib.h>
#incldue <sqlite3.h>
int main()
{
sqlite3 *db;
int ret = sqlite3_open("my.db",&db);
if(ret != 0){
printf("%s\n",sqlite3_errmsg(db));
return -1;
}
....//数据库操作
sqlite3_close(db);
return 0;
}
//编译时需要链接sqlite3
gcc -o test test.c -lsqlite3
sqlite特殊函数
typedef int (*sqlite3_callback)(void *,int,char **,char **);
int sqlite3_exec(sqlite3 *db,const char *sql,sqlite3_callback callback,void *,char **errmsg);
/*
*功能:执行sql操作
*db:数据库句柄
*sql:sql语句
*callback:回调函数
*errmsg:错误信息指针的地址
*返回值:成功返回0,失败返回错误码
*/
eg:
sqlite3 *db;
char *errmsg;
...
int ret = sqlite3_exec(db,"delete from table where id ='1';",NULL,NULL,&errmsg)
if(ret != 0){
printf("%s\n",errmsg);
return -1;
}
typedef int (*sqlite3_callback)(void *para,int f_num,char **f_value,char **f_name);
/*
*功能:没找到一条记录自动执行一次回调函数
*para:传递给回调函数的参数
*f_num:记录中包含的字段数目
*f_value:包含每个字段值得指针数组
*f_name:包含每个字段名称的指针数组
*返回值:成功返回0,失败返回-1
*/
eg:
int callback(void *para,int f_num,char **f_value,char **f_name)
{
int i;
printf("******************\n");
for(i = 0; i < f_num; i++){
printf("%s:%s\n",f_name[i],f_value[i]);
}
return 0;
}
int sqlite3_get_table(sqlite3 *db,const char *sql,char ***resultp,int *nrow,int *ncolumn,char **errmsg);
/*
*功能:执行SQL操作
*db:数据库句柄
*resultp:用来指向sql执行结果的指针
*nrow:满足雕件的记录的数目
*ncolumn:每条记录包含的字段数目
*errmsg:错误信息指针的地址
*返回值:成功返回0,失败返回错误码
*/
sqlite3 *db;
char *errmsg;
char **resultp;
int nrow;
int ncolumn;
...
int ret = sqlite3_ge_table(db,sql,&&resultp,&nrow,&ncolumn,&errmsg);
if(ret != 0 ){
printf("%s\n",errmsg);
return -1;
}
for(int i = 0; i < nrow;i++){
for(int j = 0;j < ncolumn; j++){
printf("%s",resultp[i*nrow+j]);
}
printf("\n");
}