Linux——TCP/UDP网络编程
网络相关概念:
OSI标准理论模型 7层网络模型由上到下为物理层—数据链路层—网络层—传输层—会话层—表示层—应用层。但是TCP/IP的理论实施分为四层实现,是网络分层实现的模型。
OSI标准七层与TCP/IP四层实施的对应关系
CS架构:(client serve,客户端服务器架构)
BS架构:(broswer serve浏览器服务器架构)
TCP/UDP对比与字节序概念
TCP/UDP对比
1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的即发送
数据之前 不需要建立连接
2.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差,不怕丢包、乱序等。
错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保
证可靠交付,
3.TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流
UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源
主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会
议等)
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对 一和多对多的交互通信
5.TCP首部开销20字节UDP的首部开销小,只有8个字节
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
TCP协议知识点:
TCP协议工作在传输层,对上服务socket接口,对下调用IP层,TCP面向连接,建立连接后才能进行双向通信。通信前必须先进行3次握手建立连接关系才能开始通信
(1)建立连接需要三次握手
(2)建立连接的条件:服务器listen时客户端主动发起connect才会进行三次握手
TCP三次握手示意图
TCP如何保证可靠传输
(1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
(2)TCP的接收双方收到数据包后会ack(回应)给对方,若发送方未收到ack(回应)会丢包重传
(3)TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏
(4)TCP会根据网络带宽来自动调节适配速率(滑动窗口技术)
(5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。
字节序概念
字节序是指多字节数据在计算机内存或计算机网络传输时各字节的存储顺序。
小端字节序(Little endian):低字节序放在低地址,低位存放再内存的其实地址。
大端字节序(Big endian): 高位存放再内存的起始地址。
网络字节序=大端字节序,其中X86CPU使用的时小端字节序,所以在进行网络数据传输的过程要进行字节序的转换,如果字节序不一致,会导致数据在传输过程中损坏。
TCP Server实现步骤
1创建套接字: 调用socket()函数创建套接字,返回一个类似于文件描述符socketfd的网络描述符.
函数原型:int socket(int domain,int type,int protocol);
· domain:
指明所使用的协议族,通常为AF_INET表示互联网协议族(TCP/IP 协议族);
AF_INET IPv4 因特网域
AF_INET6 IPv6 因特网域
AF_UNIX Unix域
AF_ROUTE 路由套接字
AF_KEY 密钥套接字
AF_UNSPEC 末指定
· type参数指定 socket 的类型:
SOCK STREAM:(TCP的特点)
流式套接字提供可靠的、面向连接的通信流:它使用TCP协议,从而保证了数据传输的正确性和顺序性
SOCK DGRAM:(UDP)的特点
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的并且不保证是可靠、无差错的。它使用数据报协议UDP。
SOCK RAW
允许程序使用低层协议,原始套接字允许对底星协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
· protocol
通常赋值为“0”
· 0选择type类型对应默认的协议
· IPPROTO_TCP TCP传输协议
· IPPROTO_UDP UDP传输协议
· IPPROTO_SCTP SCTP传输协议
· IPPROTO_TIPC TIPC传输协议
2为套接字添加信息: 添加IP地址和端口号调用bind()函数进行绑定。函数原型:
int bind(int socketfd , const struct sockaddr *addr, socklen_t addrlen);
功能
· 用于绑定IP地址和端口号到socketfd(),
参数
· sockfd
是 socket()函数返回的socketfd网络描述符
·addr
是 指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指
针,指向要绑定给 sockfd的协议地址结构,这个地址结构根据地址创
建socket 时的地址协议族的不同而不同
ipv4对应的是:
struct sockaddr(
unisgned short as_family; ***********************协议族
char sa_data[14]; *********************************端口
同等替换:
struct sockaddr_in{
sa_family_t sin_family;***************************协议族
in_port_t sin_port; *********************************端口号
struct in_addr sin_addr; *************************IP地址结构体
unsigned char sin_zero[8];*********填充 没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换
}
IP地址转换API:
int inet_aton(const char * straddr,struct in_addr*addrp);用于把字符串形式的“192.168.1.123”转换为网络能识别的格式。
第一个参数为要进行转换的字符串比如可以直接把IP地址“192.168.1.123”放入其中。
第二个参数为转换后的参数放到的结构体位置的地址。
char*inet_ntoa(struct int_addr inaddr);
把网络格式的IP地址转化为字符串形式。
字节序转换api
#include<netinet/in.h>
uint16t htons(uint16 t host16bitvalue); //返回网络字节序的值
uint32t htonl(uint32 t host32bitvalue); //返回网络字节序的值
uint16t ntohs(uint16 t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32 t net32bitvalue); //返回主机字节序的值
h代表host,n代表net,s代表short(两个字节),|代表long (4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用IlNADDR_ANY,INADDR_ANY指定地址让操作系统自己获取
3监听网络连接: 调用listen()函数。
函数原型int listen(int socketfd,int backlog);
功能
•设置能处理的最大连接数,listen()并未开始接受连线,只是设置sockect的listen模式,listen 函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
内核为任何一个给定监听套接字维护两个队列:
•未完成连接队列,每个这样的 SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于SYN_REVD 状态;
·已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED状态:
参数
sockfd
sockfd 是socket 系统调用返回的服务器端 socket 描述符
backlog
backlog 指定在请求队列中允许的最大请求数
4监听到有客户接入,接受一个连接: 调用accept()函数连接,函数原型如下:
int accept (int socketfd,struct sockaddr*addr,socklen_t * addrlen);
功能
accept函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
参数
sockfd
sockfd 是 socket 系统调用返回的服务器端 socket 描述符
addr
用来返回已连接的对端(客户端)的协议地址(如果不关心可以传NULL)可以调用char*inet_ntoa(struct in_addr inaddr);函数把网络格式的IP地址转化为字符串形式。
addrled
客户端地址长度(如果不关心可以传NULL)
返回值
该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
连接完成即可进行数据的收发: :调用read()和write()函数进行数据的收发操作。
ssize_t write(int fd,const void*buf,size_t nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);
函数均发回读或写的字节数,错误则返回-1
第一个将 buf中的nbytes个字节写入到文件描述符 fd 中,成功时返回写的字节数。第二个为从 fd中读取nbyte个字节到buf中,返回实际所读
的字节数。详细应用说明参考使用read write读写socket(套节字)。
网络I/0还有一些函数,例如:recv()/ send(), readv()/writev() recvmsg()/sendmsg(), recvfrom()/sendto 0等
关闭套接字断开连接: 调用函数close()关闭套接字,断开连接。
服务器部分代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
char buf[1024]; //定义接收缓冲区
int readlen=0; //定义数据接收长度
char ip[30]={0}; //定义ip地址保存变量
int port_num=0; //定义一个整型变量保存对方的端口号
//1创建监听套接字
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if (sockfd==-1)
{
printf("creat socket error\n");
perror("why");
}
//2绑定我们本地的IP地址和端口号
struct sockaddr_in addr;
addr.sin_family=AF_INET; //协议族
// uint16_t htons(uint16_t hostshort);用于主机字节序转网络字节序,网络端口为大端字节序
addr.sin_port=htons(8999); //设置端口号
// int inet_pton(int af, const char *src, void *dst);以IP地址的点乘十进制的形式装换成网络字节序
//addr.sin_addr.s_addr=inet_pton(AF_INET,"192.168.2.224",&addr.sin_addr.s_addr); //设置网络地址
inet_aton("192.168.2.224",&addr.sin_addr); //将字符串IP地址转换成32位的网络序列IP地址
// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret=bind(sockfd,(struct sockaddr*)&addr,(socklen_t)sizeof(addr));
if (ret==-1)
{
printf("bind socket error\n");
perror("why");
}
//3设置监听
//int listen(int sockfd, int backlog); 参数backlog为设置的最大监听数
ret=listen(sockfd,128);
if(ret==-1)
{
printf("bind listen error\n");
perror("why");
}
//阻塞等待,链接到达。链接成功后返回通信用的套接字
struct sockaddr_in caddr; //定义接收套接字的结构体
socklen_t addrlen=sizeof(caddr);
//int accept(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags); 成功会返回一个用于连接的套接字
int fcltfd=accept(sockfd,(struct sockaddr*)&caddr,&addrlen);
if(fcltfd==-1)
{
printf("bind accept error\n");
perror("why");
}
//5开始通信
while(1)
{
memset(buf,0,sizeof(buf));
//用于将网络字节序的ip地址转换为主机字节序的点乘10进制,将src里面的ip地址数据储存到dst这个变量
//int inet_pton(int af, const char *src, void *dst);
inet_ntop(AF_INET,(char*)&(caddr.sin_addr.s_addr),ip,sizeof(ip));
port_num=ntohs(caddr.sin_port); //将网络字节序的端口号转换成主机字节序的整型数返回
readlen=read(fcltfd,&buf,1024);
//printf("this is ip addr:%s\n",ip);
send(fcltfd,"from serve message",strlen("from serve message")+1,0);
if (readlen<0)
{
printf("read data from tcp error\n");
perror("why");
break;
}
else if (readlen==0)
{
//6通信完成,关闭连接套接字和通信套接字
close(sockfd);
close(fcltfd);
printf("已断开连接\n");
break;
}
else if (readlen>0)
{
printf("this is buf:%s\n",buf);
write(readlen,"hello form tcp",16);
}
}
return 0;
}
TCP Client实现步骤
1创建socket通道 调用socket函数实现,
2连接 :调用connect()函数连接,客户端发起connect()连接请求,服务器的listen()函数监听到有新的连接接入,服务器调用accept()进行连接.函数原型:
int connect(int socketfd,const struct socketaddr*addr,socklen_t addrlen);
功能
该函数用于绑定之后的client 端(客户端),与服务器建立连接参数
• sockfd
是目的服务器的 sockect 描述符
• addr
是服务器端的 IP 地址和端口号的地址结构指针
addrlen
地址长度常被设置为sizeof(struct sockaddr)
返回值
成功返回o,遇到错误时返回-1,并且errno中包含相应的错误码
客户端部分代码展示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
char buf[1024]; // 接收缓冲区
int num=0;
//1建立套接字连接
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if (sockfd==-1)
{
printf("creat socket error\n");
perror("why");
}
//2连接服务器
struct sockaddr_in addr;
//addr.sin_addr.s_addr=inet_pton(sockfd,"192.168.2.224",&addr.sin_addr.s_addr); 这种ip地址配置会导致程序阻塞在connect连接处
inet_aton("192.168.2.224",&addr.sin_addr); //将IP地址的主机字节序转换成网络字节序
addr.sin_family=AF_INET;
addr.sin_port=htons(8999);
// int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int ret=connect(sockfd,(struct sockaddr *)&addr,(socklen_t)sizeof(addr));
printf("program test\n");
if (ret==-1)
{
printf("connect error\n");
perror("why");
}
//3通信
while(1)
{
num++;
sprintf(buf,"hello serve%d",num);
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send(sockfd,buf,strlen(buf)+1,0);
memset(buf,0,1024);
//ssize_t recv(int sockfd, void *buf, size_t len, int flags);
int len=recv(sockfd,buf,sizeof(buf),0);
if (len>0)
{
printf("服务器:%s\n",buf);
}
else if (len==0)
{
printf("断开连接\n");
break;
}
else if (len<0)
{
printf("读取失败\n");
perror("why");
break;
}
}
//4关闭套接字
close(sockfd);
return 0;
}