1.网络以覆盖范围划分:局域网/城域网/广域网? ?互联网/因特网? ?以太网/令牌环网--组网方式
2.在网络中必须能够为一表示每一台主机,才能实现点到点的精确通信
? ? ? ? ? ?IP地址:? ipv4:uint32_t 无符号4个字节的整数? ? DHCP/NAT? ? ipv6:uint_t addr[16];
网络通信中的每条数据都必须具备:源IP地址/目的IP地址--表示数据从哪个主机来,到哪个主机去。
? ? 目的IP地址:能够让网络中的路由器为每一条数据根据目的地址选择不同的路径到达对端主机。
? ? 源IP地址:能够让对端主机知道数据是谁发送,以便于回复数据。
3.IP地址使网络中实现主机与主机之间的通信,但是主机上有很多进程:通信中必须标识一条数据应该由哪个进程处理
? ? ? ? ? ?端口:uint16_t 无符号2个字节的整数 0~65535? ?
?一个进程可以使用多个端口,但是一个端口只能被一个进程占用。
网络中的每条数据都必须具备:源端口/目的端口? ?表示数据从哪个进程来,到哪个进程去。
4.为什么不使用pid标识进程而是使用新的字段端口标识?
进程的pid会随着程序的重启发生改变,但是端口不会。
5.通过IP地址和端口可以实现不同主机之间的进程间通信(网络通信)
? ? ? ? ? ? ? ? ? 协议:约定? ? --(约定好的共同的规则)
? ? ? ? ? ? ? ? ? 网络通信协议:网络通信中数据格式的约定
6.协议分层:
? ? ? ? ? ? ? ? 对通信协议在不同的通信环境中进行封装,不同层次使用不同协议,提供不同的服务;将通信环境划分出来,通信的实现更加简单,更容易形成规范。
网络通信环境中的协议分层
ISO:OSI七层参考模型:应用层/表示层/会话层/传输层/网络层/链路层/物理层
TCP/IP五层(或四层)模型:应用层/传输层/网络层/链路层/物理层(考虑较少)
TCP/IP是一组协议栈/协议簇:其中包含许多协议,IP和TCP协议只是其中比较典型的两种。
应用层:负责应用程序之间的数据沟通;例如qq与qq之间的通信协议协商qq的数据格式;典型协议:HTTP/DNS/FTP
传输层:负责不同主机上进程间的数据传输;因为传输层的协议中包含主要信息就是端口;典型协议:TCP/UDP
网络层:负责地址管理与路由选择;为网络中的数据选择合适的路径;IP信息;典型协议:IP;典型设备:路由器
链路层:负责相邻设备之间的数据帧识别以及传输;网卡设备的MAC地址信息;典型协议:Ethernet;典型设备:交换机
物理层:负责物理光电信号的传输;典型协议:以太网协议
协议分层:根据提供的服务不同,分出不同的通信层次,在每个层次使用不同的协议实现数据格式约定---将复杂的网络通信环境简化清晰。
网络通信中的数据传输流程(传输层):
字节序:cpu在内存中对数据进行存取的顺序--取决于cpu架构
cpu架构:X86结构cpu (小端)/mips架构cpu(大端)
0x01020304 01是高位? 04是低位
大端字节序:低地址存高位 a[0]=01? a[1]=02? a[2]=03? a[3]=04
小端字节序:低地址存低位 a[0]=04? a[1]=03? a[2]=02? a[3]=01
网络通信是不同主机之间的通信,但是不同主机上的字节序会对通信造成极大影响:造成数据二义
因此网络通信中必须统一字节序--网络字节序,才能避免出现这种问题
不管主机是大端还是小端,网路通信时统一转换为网络字节序--大端字节序
若通信主机是小端主机,则网络通信时将数据进行字节序转换才能发送
并不是所有数据都需要转换:需要转换的数据关键在于内存中一次存取大小超过一个字节的数据:short int long float double
但是字符串 char buf[1024]是不需要转换的--字符串本身就是按字节存储
需要转换的是小端主机,大端不需要--如何判断一个主机的字节序是大端还是小端?
union tmp(int a;char b(b就是a[0]);)? tmp.a=1 if(tmp.b=1)(小端)
Socket套接字编程:
socket是一套网络编程接口,类似于中间件;上层用户可以通过这些接口简单完成网络通信传输;而不需要过于关心内部的实现过程。套接字编程讲的就是使用socket接口实现网络通信
五元组:源IP地址/源端口/目的IP地址/目的端口/通信协议--标识一条通信--每一条网络中的数据都会包含信息。
socket编程:TCP/UDP(都是双工通信)
传输层有2个协议:TCP/UDP;这2个协议各不同,因此实现流程也稍有差别;
UDP:用户数据报协议;无连接,不可靠,面向数据报;
? ? ? ? ? 不关心数据是否响应,是否丢了。
? ? ? ? ? 应用场景:数据实时性大于安全性的场景--视频传输
TCP:传输控制协议:面向连接,可靠传输,面向字节流;
? ? ? ? ? ?先建立连接,服务器不响应,则连接无法建立
? ? ? ? ? ?应用场景:数据安全性大于实时性--文件传输
面向数据报:无连接的,不可靠的,无需的,有最大长度限制的数据传输服务
面向字节流:基于连接的,可靠的,有序的,双向的字节流传输服务? 以字节为单位,不限制上层传输数据大小的传输方式。
网络通信中两个主机进程间的通信:客户端/服务器端
? ? ? ? ? ? ? ? ? ? ?客户端:主动发出请求的一方? ? ?服务器端:被动接受请求的一方?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?永远都是客户端->服务器端发送请求
(意味着客户端必须知道服务器端的地址信息才可以在发送数据时,将数据能够i层层封装完成)
网络传输的数据都应该包括:源IP地址/目的IP地址/源端口/目的端口/协议
UDP网络编程流程:
服务器端:(地址永久不变)
1.创建套接字--在内核中创建socket结构体;向进程返回一个操作句柄;
通过这个内核中的socket结构体与网卡建立联系
2.为套接字绑定地址信息--向内核中创建的socket结构体添加各种地址描述信息(IP地址和端口)
? ? ? ? 绑定地址是为了告诉操作系统,我使用了哪个地址和端口你接收到了数据,若目的地址信息和我绑定的地址信息相同,则将数据交给我处理,并且发送数据的时候,源端地址信息就是绑定的地址信息。---服务器和客户端自己绑定自己的地址信息。 操作系统接收到数据,会判断目的地址的信息,去内核中socket容器一一查找,找到则将这个数据放入socket接收的缓冲区中,没找到则直接丢弃。
3.接收数据--从socket结构体中的接收缓冲区中取出数据
? ? ? ? ? ? ? ?每个数据中都包含源地址和目的地址,因此获取数据也就获悉了对端是谁
4.发送数据--把数据拷贝到内核中的socket结构体的发送缓冲区中
? ? ? ? ? ? ? ?操作系统这时会在合适的时候从发送缓冲区取出数据,然后进行数据的层层封装,通过网卡发送
5.不通信则关闭套接字,释放资源
客户端:
? ? ? ? ? ? ? ?1.创建套接字
? ? ? ? ? ? ? ?2.为套接字绑定地址信息
? ? ? ? ? ? ? ?3.发送数据
? ? ? ? ? ? ? ?4.接收数据
? ? ? ? ? ? ? ?5.关闭套接字?
socket接口的介绍:
1.创建套接字
int socket(int domain, int type, int protocol);
? ? ? ? ? ? ?domain:地址域--不同的网络地址结构? AF_INET? -ipv4地址域
? ? ? ? ? ? ?type:套接字类型--流式套接字/数据报套接字
流式套接字:一种有序的,可靠的,双向的,基于连接的字节流传输? SOCK_STREAM
数据报套接字:无连接的,不可靠的,有最大长度限制的传输? ? SOCK_DGRAM
? ? ? ? ? ? ? ? ?protocol:使用的协议 0--不同套接字类型下的默认协议;流式套接字默认是TCP/数据报套接字默认是UDP
? ? ? ? ? ? ?? IPPROTO_TCP--TCP协议? ? ? ? ? ? ?IPPROTO_UDP---UDP协议
? ? ? ? ? ? ? ?返回值:返回套接字的操作句柄---文件描述符
2.为套接字绑定地址信息(为了实现接口统一,转换为统一的网络字节序)
int bind(int sockfd, const struct sockaddr *addr,?socklen_t len);
sockfd:创建套接字返回的操作句柄
addr:要绑定的地址信息结构
len:地址信息的长度
返回值:成功返回0;失败返回-1
? ? ? ? ? ? ? ? ?用户先定义sockaddr_ in的IPV4地质结构,强转之后传入bind中
? ? ? ? ? ? ? ? ? ?bind(sockaddr*){
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if(sockaddr->sin_family==AF_INET){
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//这是ipv4地址结构的解析?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }else if(sockaddr->sin_family==AF_INT6)
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ? bind((struct sockaddr*)&addr)
3.发送数据
int sendto(int sockfd,char *data,int data_len,int flag,struct sockaddr *dest_addr,socklen_t len)
sockfd:套接字操作句柄,发送数据就是将数据拷贝到内核的socket发送缓冲区中
data:要发送的数据的首地址
data_len:要发送的数据的长度
flag:选项参数? 默认为0--表示当前操作是阻塞操作? MSG_DONTWAIT--设置为非阻塞
? ? ? ? ? ?若发送数据的时候,socket发送缓冲区已经满了,则0默认阻塞等待;MSG_DONTWAIT立即报错返回
dest_addr:目的端地址信息结构--表示数据要发送给谁
? ? ? ? ? ? ? ? ? 每一条数据都要描述源端信息(绑定的地址信息)和对端信息(当前赋予的信息)
addr_len:地址信息结构长度
返回值:成功返回实际发送的数据字节数:失败返回-1
4.接收数据
int recvfrom(int sockfd,char *buf ,int len, int flag, struct sockaddr *src_addr,socklen_t *addr_len);
sockfd:套接字操作句柄
buf:缓冲区的首地址,用于存放接收到的数据,从内核socket接收缓冲区中取出数据放入这个buf用户态缓冲区中
len:用户想要读取的数据长度,但是不能大于buf缓冲区的长度
flag:0-默认阻塞操作? -若缓冲区中没有数据则一直等待? MSG_DONTWAIT-非阻塞
src_addr:接收到的数据的发送端地址--表示这个数据是谁发的,从哪来--回复的时候就是对这个地址进行回复
addr_len:输入输出型参数,用于指定想要获取多长的地址信息;获取地址之后,用于返回地址信息的实际长度
返回值:成功返回实际接收到的数据字节长度;失败返回-1;
5.关闭套接字
int close(int fd);
客户端程序中流程的特殊之处:
客户端通常不推荐用户自己绑定地址信息,而是让操作系统在发送数据的时候发送socket还没有绑定地址,然后自动选择一个合适的ip地址和端口进行绑定
? ? ? ?1.如果不主动绑定,操作系统会选择一个合适的的地址信息进行绑定(什么地址就是合适的地址?--当前没有被使用的端口)
? ? ? ? ? 一个端口只能被一个进程占用,若用户自己指定端口以及地址进行绑定有可能这个端口已经被使用了,则会绑定失败,让操作系统选择合适的端口信息,可以尽最大能力避免端口冲突的概率。
对于客户端来说,其实并不关心使用什么源端地址将数据发送出去,只要能够发送数据并且接收数据就行
? ? ? ? 2.服务端可不可以也不主动绑定地址?---不可以
? ? ? ? ?客户端所知道的服务端的地址信息,都是服务端告诉客户端
? ? ? ? ?一旦服务端不主动绑定地址,则会造成操作系统队医选择合适的地址进行绑定,服务端自己都不确定自己用了什么地址信息,如何告诉客户端,因此服务端通常必须主动绑定地址,并且不能随意改动
UDP服务端程序:使用C语言编写
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <netinet/in.h>//sockaddr结构体/IPPROTO_UDP
5 #include <arpa/inet.h>//包含一些字节序转换的接口
6 #include <sys/socket.h>//套接字接口头文件
7 #include <stdlib.h>
8 //编写一个udp服务端的C语言程序
9 //1.创建套接字
10 //2.为套接字绑定地址信息
11 //3.接收数据
12 //4.发送数据
13 //5.关闭套接字
14 int main(int argc,char *argv[]){
15
16 //argc表示参数个数,通过argv向程序传递端口参数
17 if(argc!=3){
18 printf("./udp_srv ip port em:./udp_srv 192.168.122.132 9000\n");
19 return -1;
20 }
21 const char *ip_addr=argv[1];
22 uint16_t port_addr=atoi(argv[2]);
23 //socket(地址域,套接字类型,协议类型)
24 //1.创建套接字
25 int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
26 if(sockfd<0){
27 perror("socket error");
28 return -1;
29 }
30 //bind(套接字描述符,地址结构,地址长度);
31 //struct sockaddr_in ipv4地址结构
32 struct sockaddr_in addr;
33 addr.sin_family=AF_INET;
34 //htons--将2个字节的主机字节序整数转换为网络字节序的整数
35 addr.sin_port=htons(port_addr);
36 //inet_addr 将一个点分十进制的字符串IP地址转换为网络字节序的整数IP地址
37 addr.sin_addr.s_addr=inet_addr(ip_addr);
38 socklen_t len=sizeof(struct sockaddr_in);
39 int ret=bind(sockfd,(struct sockaddr*)&addr,len);
40 if(ret<0){
41 perror("bind error");
42 return -1;
43 }
44 //3.接收数据
45 while(1){
46 char buf[1024]={0};
47 struct sockaddr_in cliaddr;
48 socklen_t len=sizeof(struct sockaddr_in);
49 //recvfrom(描述符,缓冲区,长度,参数,客户端地址信息,地址信息长度)
50 int ret=recvfrom(sockfd,buf,1023,0,(struct sockaddr*)&cliaddr,&len);
51 if(ret<0){
52 perror("recfrom error");
53 return -1;
54 }
55 printf("client sya:%s\n",buf);
56 printf("server say:");
57 fflush(stdout);//让用户输入数据,发送给客户端
58 memset(buf,0x00,1024);//清空buf中的数据
59 scanf("%s",buf);
60 //通过sockfd将buf中的数据发送到cliaddr客户端
61 ret=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&cliaddr,len);
62 if(ret<0){
63 perror("sendto error");
64 return -1;
65 }
66 }
67 close(sockfd);//5.关闭套接字
68 }
69
?
UDP客户端程序:使用C++封装一个udpsocket类,向外提供简单接口就能实现一个客户端/服务端
1 #include <cstdio>
2 #include <unistd.h>
3 #include <string.h>
4 #include <netinet/in.h>//sockaddr结构体/IPPROTO_UDP
5 #include <arpa/inet.h>//包含一些字节序转换的接口
6 #include <sys/socket.h>//套接字接口头文件
7 #include <iostream>
8 #include <stdlib.h>//atoi接口
9 //封装一个UDPsocket类,向外提供简单的接口实现套接字编程
10 class UdpSocket{
11
12 public:
13 UdpSocket():_sockfd(-1){
14
15 }
16 //1.创建套接字
17 bool Socket(){
18 //socket(地址域,套接字类型,协议类型);
19 _sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
20 if(_sockfd<0){
21 perror("socket error");
22 return false;
23 }
24 return true;
25 }
26
27 //2.为套接字绑定地址信息
28 bool Bind(const std::string &ip,uint32_t port){
29 //(1).定义IPv4地址结构
30 struct sockaddr_in addr;
31 addr.sin_family=AF_INET;//地址域,用于向bind接口表明这是一个ipv4地址结构
32 addr.sin_port=htons(port);//htons将2个字节的主机字节序整数转换为网络字节序的整数
33 addr.sin_addr.s_addr=inet_addr(ip.c_str());//网络字节序的IP地址
34 /*
35 //bind的接口3个参数赋值信息
36 struct sockaddr_in{
37 sa_family_t sin_family;
38 in_port_t sin_port;
39 struct in_addr{
40 in_addr_t _addr;
41 }sin_addr;
42 }
43 */
44 //(2).绑定地址
45 socklen_t len=sizeof(struct sockaddr_in);
46 //bind(描述符,统一地址结构sockaddr*,地址信息长度)
47 int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
48 if(ret<0){
49 perror("bind error");
50 return false;
51 }
52 return true;
53 }
54
55 //3.发送数据
56 bool Send(const std::string &data,const std::string &ip,uint16_t port){
57 //sendto(描述符,数据,长度,选项,对端地址,地址长度)
58 //(1).定义对端地址信息的ipv4的地址结构
59 struct sockaddr_in addr;
60 addr.sin_family=AF_INET;//地址域,用于向bind接口表明这是一个ipv4地址结构
61 addr.sin_port=htons(port);//htons将2个字节的主机字节序整数转换为网络字节序的整数
62 addr.sin_addr.s_addr=inet_addr(ip.c_str());//网络字节序的IP地址
63 //(2).向这个地址发送数据
64 int ret;
65 socklen_t len=sizeof(struct sockaddr_in);
66 ret=sendto(_sockfd,data.c_str(),data.size(),0,(struct sockaddr*)&addr,len);
67 if(ret<0){
68 perror("sendto error");
69 return false;
70 }
71 return true;
72 }
73
74 //4.发送数据
75 //输入性参数使用const 引用
76 //输出型参数使用指针
77 //输入输出行使用引用
78 bool Recv(std::string *buf,std::string *ip=NULL,uint16_t *port=NULL){
79 //recvfrom(描述符,缓冲区,数据长度,选项,对端地址,地址长度)
80 struct sockaddr_in addr;//用于获取发送端地址信息
81 socklen_t len=sizeof(struct sockaddr_in);//指定地址长度以及获取实际地址长度
82 int ret;
83 char tmp[4096]={0};//临时用于存放数据的缓冲区
84 ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&addr,&len);
85 if(ret<0){
86 perror("recvfrom error");
87 return -1;
88 }
89 buf->assign(tmp,ret);//给buf申请ret大小的空间,从tmp中拷贝ret长度的数据进去
90 //为了接口灵活,用户若不想获取地址信息,则不再转换获取
91 只有当用户想要获取地址的时候,这时传入缓冲区,我们将数据写入进去
92 if(ip!=NULL){
93 *ip=inet_ntoa(addr.sin_addr);//将网络字节序整数IP地址转换为字符串地址返回
94 }
95 if(port!=NULL){
96 *port=ntohs(addr.sin_port);
97 }
98 return true;
99 }
100
101 //5.关闭套接字
102 void Close(){
103 close(_sockfd);
104 _sockfd=-1;
105 return;
106 }
107 private:
108 //贯穿全文的套接字描述符
109 int _sockfd;
110 };
111 112 #define CHECK_RET(q) if((q)==false){return -1;}
113 //客户端要给服务端发送数据,那么就需要知道服务端的地址信息
114 因此通过程序运行参数传入服务端的地址信息
115 int main(int argc,char *argv[]){
116 if(argc!=3){//表示参数的个数,看输入的参数是否为3
117 printf("em: ./udp_cli 192.168.122.132 9000\n");
118 return -1;
119
120 }
121
122 //argv[0]=./udp_cli
123 //argv[1]=192.168.122.132
124 //argv[2]=9000
125 std::string ip_addr=argv[1];//服务端地址信息
126 uint16_t port_addr=atoi(argv[2]);
127 UdpSocket sock;
128 CHECK_RET(sock.Socket());//创建套接字
129 //CHECK_RET(sock.Bind());//绑定地址信息
130 while(1){
131 //获取一个标准输入的数据,进行发送
132 std::cout<<"client say: ";
133 fflush(stdout);
134 std::string buf;
135 std::cin>>buf;//获取标准输入的数据
136 sock.Send(buf,ip_addr,port_addr);//向指定的主机进程发送buf数据
137
138 buf.clear();//清空buf缓冲区
139 sock.Recv(&buf);//因为本身客户端就知道服务端的地址,因此不需要在获取
140 std::cout<<"server say: "<<buf<<std::endl;
141 }
142 sock.Close();
143 return 0;
144 }
TCP编程流程:面向连接,可靠传输,面向字节流,UDP与之相反,但都是(双工通信)
服务端:
1.创建套接字--创建socket结构
2.为套接字绑定地址信息(告诉操作系统哪些数据放到我的socket缓冲区,发送数据使用哪个源端地址信息进行发送)
3.开始监听(TCP两端通信之前需要先建立链接--确保通信双方都具有收/发数据的能力;而建立连接的过程是操作系统完成的,用户不需要关心;开始监听就相当于告诉操作系统,可以开始接收客户端的连接请求了)
? ? ? ? ? ? ? ? ? ? ?(1)服务的创建套接字可以接收客户端的连接请求
? ? ? ? ? ? ? ? ? ? ?(2)当客户端连接请求到来之后,服务端会为这个客户端单独创建一个socket
socket结构体中描述(源端IP/源端port/对端IP/对端port/协议...);这些描述信息表示这个socket用于专门跟这个客户端进行通信
? ? ? ? ? ? ? ? ? ? ? 服务端会为每一个客户端创建一个socket实现与客户端通信 (与UDP的不同)
? ? ? ? ? ? ? ? ? ? ?最早的创建套接字:监听套接字--就像门迎--永远只接收客户端的连接请求
? ? ? ? ? ? ? ? ? ? ?创建新的套接字:通信套接字--后续与客户端数据通信--就像一对一服务员与这个客户端进行数据通信。有多少客户端意味着有多少套接字socket结构体与其对应
4.服务端程序中获取这个新建套接字的操作句柄描述符
? 因为后续与这个客户端的通信都是通过这个操作句柄完成的
最早的创建套接字描述符--操作句柄只是用于建立连接,获取新连接的
5.收发数据:因为TCP套接字socket既描述了源端,也描述了对端,因此收发数据不需要在获取/指定对端的地址信息。(与UDP的不同之处)
6.关闭套接字:释放资源
UDP服务端不存在照样能发数据,TCP则直接拒绝连接(与UDP不同)
?
客户端:
1.创建套接字
2.绑定地址信息(不推荐)
3.向服务端发起连接请求
4.收发数据
5.关闭套接字
TCP客户端与服务端流程:
客户端:创建套接字,描述地址信息,发起连接请求,连接建立成功,收发数据,关闭
服务端:创建套接字,描述地址信息,开始监听,接收连接请求,新建套接字,获取新建套接字描述符,通过这个描述符与客户端通信,关闭。
TCP编程socket接口介绍:
1.创建套接字:?int socket(int domain, int type, int protocol);? ?(AF_INET,SOCK_STREAM--流式套接字,IPPROTO_TCP)
2.绑定地址信息:int bind (int sockfd,syruct sockaddr *addr,socklen_t len);? ?struct sockaddr_in;
3.服务端开始监听:int listen(int sockfd,int backlog);---告诉操作系统开始接收连接请求
? ? ? ? ? ? ? ?backlog:决定同一时间,服务端所能接收的客户端连接请求数量--最大节点数量,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?但并不能决定服务端能够接收多少客户端的参数。
? ? ? ? ? ? ?万一有网络攻击:SYN泛洪攻击:恶意主机不断地向服务端主机发送大量的连接请求
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?若服务端为每一个连接请求建立socket,则会瞬间资源耗尽,服务器崩溃
? ? ? ? ? ? ? 因此服务端有一个connection pending queue;存放为连接请求新建的socket节点;backlog参数决定了这个队列的最大节点数量;若这个队列放慢了,若还有新连接请求到来,则将 这个后续请求丢弃掉。
4.获取新建socket的操作句柄:从内核指定socket的pending queue中取出一个socket,返回操作句柄
int accept(int sockfd,struct sockaddr *addr,socklen_t *len);
sockfd:监听套接字--指定要获取哪个pending queue中的套接字
addr:获取一个套接字,这个套接字与指定的客户端进行通信,通过addr获取这个客户端的地址信息
len:输入输出型参数---指定地址信息想要的长度以及返回实际的地址长度
返回值:成功则返回新获取的套接字的描述符---操作句柄
5.通过新获取的套接字操作句柄(accept返回的描述符)与指定客户端进行通信
接收数据:ssize_t recv(int sockfd,char *buf,int len,int flag); 返回值:成功返回实际读取的数据长度;连接断开返回0;读取失败返回-1. ---TCP连接断开与管道读取数据一样,所有写段被关闭,读端返回0
发送数据:ssize_t send(int sockfd,char *data,int len,int flag);返回值:成功返回实际发送的数据长度;失败返回-1;若连接断开触发异常
6.关闭套接字:释放资源? int close(int fd)
7.客户端服务端发送连接请求
int connect(int sockfd,int sockaddr *addr,socklen_t len);
sockfd:客户端套接字---若还未绑定地址,则操作系统会选择合适的源端地址进行绑定
addr:服务端地址信息---struct sockaddr_in;这个地址信息经过connect之后也会描述到socket中
len:地址信息长度
字节序转换接口:
uint6_t htons(uint16_t port);//2个字节的数据
uint6_t ntohs(uint16_t port);//主机字节序与网络字节序
uint32_t htonl(uint32_t port);//4个字节的数据
uint32_t ntohl(uint32_t port);//主机字节序与网络字节序
in_addr_t inet_addr(char *ip);//点分十进制字符串IP地址转换为网络字节序整数IP地址
char *inet_ntoa(struct in_addr addr);//网络字节序整数IP地址转换为点分十进制字符串
int inet_pton(int domain,char *ip,void *ip);//字符串地址转换网络字节序整数地址,domain地址域
int inet_ntop(int domain,void?*ip,char?*ip,int len);//网络字节序整数IP地址转换为点分十进制字符串
实现代码如下:包括tcpsocket.hpp?
1 #include <cstdio>
2 #include <unistd.h>
3 #include <string.h>
4 #include <netinet/in.h>//sockaddr结构体/IPPROTO_UDP
5 #include <arpa/inet.h>//包含一些字节序转换的接口
6 #include <sys/socket.h>//套接字接口头文件
7 #include <iostream>
8 #include <stdlib.h>//atoi接口
9 //封装实现一个tcpsocket类,向外界提供简单接口
10 //使外部通过实例化一个tcpsocket对象就能完成tcp通信程序的建立
11 #define BACKLOG 10
12 #define CHECK_RET(q) if((q)==false){ return -1;}
13 class TcpSocket
14 {
15 public:
16 TcpSocket():_sockfd(-1){
17
18 }
19 //1.创建套接字
20 bool Socket(){
21 //socket(地址域,套接字类型,协议类型)
22 _sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
23 if(_sockfd<0){
24 perror("socket create error");
25 return false;
26 }
27 return true;
28 }
29 void Addr(struct sockaddr_in *addr,const std::string &ip,uint16_t port){
30 addr->sin_family=AF_INET;
31 addr->sin_port=htons(port);
32 inet_pton(AF_INET,ip.c_str(),&(addr->sin_addr.s_addr));
33 }
34 //2.绑定地址信息
35 bool Bind(const std::string &ip,const uint16_t port){
36 //(1)定义IPv4地址结构
37 struct sockaddr_in addr;
38 Addr(&addr,ip,port);
39 socklen_t len=sizeof(struct sockaddr_in);
40 int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
41 if(ret<0){
42 perror("bind error");
43 return false;
44 }
45 return true;
46 }
47 //3.服务端开始监听
48 bool Listen(int backlog=BACKLOG){
49 //Listen(描述符,同一时间的并发连接数);
50 int ret=listen(_sockfd,backlog);
51 if(ret<0){
52 perror("listen error");
53 return false;
54 }
55 return true;
56 }
57 //4.客户端发起连接请求
58 bool Connect(const std::string &ip, const uint16_t port){
59 //(1)定义IPv4地址结构,赋予服务端地址信息
60 struct sockaddr_in addr;
61 Addr(&addr,ip,port);
62 //(2)向服务端发起请求
63 //(3)conect(客户端描述符,服务端地址信息,地址长度)
64 socklen_t len=sizeof(struct sockaddr_in);
65 int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
66 if(ret<0){
67 perror("connect error");
68 return false;
69 }
70 return true;
71 }
72 //5.服务端获取新建连接
73 bool Accept(TcpSocket *sock,std::string *ip=NULL,uint16_t *port=NULL){
74 //accept(监听套接字,对端地址信息,地址信息长度)返回新的描述符
75 struct sockaddr_in addr;
76 socklen_t len=sizeof(struct sockaddr_in);
77 //获取新的套接字,以及这个套接字对应的对端地址信息
78 int clisockfd=accept(_sockfd,(struct sockaddr*)&addr,&len);
79 if(clisockfd<0){
80 perror("accept error");
81 return false;
82 }
83
84 //为这个对象的描述符进行赋值--赋值为新建的套接字的描述符
85 后续与客户端的通信通过这个对象就可以完成
86 sock->_sockfd=clisockfd;//用户传入了一个Tcpsocket对象的指针
87 if(ip!=NULL){
88 *ip=inet_ntoa(addr.sin_addr);//将网络字节序整数IP地址转换为字符串地址返回
89 }
90 if(port!=NULL){
91 *port=ntohs(addr.sin_port);
92 }
93 return true;
94 }
95 //6.发送数据
96 bool Send(const std::string &data){
97 //send(描述符,数据,数据长度,选项参数)
98 int ret=send(_sockfd,data.c_str(),data.size(),0);
99 if(ret<0){
100 perror("send error");
101 return false;
102 }
103 return true;
104 }
105 //7.接收数据
106 bool Recv(std::string *buf){
107 //recv(描述符,缓冲区,数据长度,选项参数)
108 char tmp[4096]={0};
109 int ret=recv(_sockfd,tmp,4096,0);
110 if(ret<0){
111 perror("recv error");
112 return false;
113 }else if(ret==0){
114 printf("connection break\n");
115 return false;
116 }
117 buf->assign(tmp,ret);//从tmp中拷贝ret大小的数据到buf中
118 return true;
119 }
120 //8.关闭套接字
121 bool Close(){
122 close(_sockfd);
123 _sockfd=-1;
124 }
125 private:
126 int _sockfd;
127 };
tcp_cli.cpp
1 #include <iostream>
2 #include "tcpsocket.hpp"
3 #include <stdlib.h>
4
5 //使用封装的Tcpsocket类实例化对象实现tcp服务端程序
6
7 int main(int argc,char *argv[]){
8 if(argc!=3){//表示参数的个数,看输入的参数是否为3
9 printf("em: ./tcp_cli 192.168.122.132 9000--服务绑定的地址\n");
10 return -1;
11 }
12 std::string ip=argv[1];
13 uint16_t port =atoi(argv[2]);//stoi将字符串转换为数字
14
15 TcpSocket cli_sock;
16 //创建套接字
17 CHECK_RET(cli_sock.Socket());
18 //绑定地址信息(不推荐)
19 //CHECK_RET(cli_sock.Bind(ip,port));
20 //向服务端发起请求
21 CHECK_RET(cli_sock.Connect(ip,port));
22 //循环收发数据
23 while(1){
24 printf("client say:");
25 fflush(stdout);
26 std::string buf;
27 std::cin>>buf;
28
29 //因为客户端不存在多种套接字的文件,因此一旦当前套接字出错直接退出就行
30 //进程退出就会释放资源,关闭套接字
31 CHECK_RET(cli_sock.Send(buf));
32 buf.clear();
33 CHECK_RET(cli_sock.Recv(&buf));
34 printf("server say:%s\n",buf.c_str());
35 }
36 cli_sock.Close();
37 return 0;
38 }
~
?tcp_srv.cpp
1 #include <iostream>
2 #include <stdlib.h>
3 #include "tcpsocket.hpp"
4
5
6 //使用封装的Tcpsocket类实例化对象实现tcp服务端程序
7
8 int main(int argc,char *argv[]){
9 if(argc!=3){//表示参数的个数,看输入的参数是否为3
10 printf("em: ./tcp_srv 192.168.122.132 9000\n");
11 return -1;
12 }
13 std::string ip=argv[1];
14 uint16_t port =atoi(argv[2]);//stoi将字符串转换为数字
15
16 TcpSocket lst_sock;
17 CHECK_RET(lst_sock.Socket());//创建套接字
18 CHECK_RET(lst_sock.Bind(ip,port));//绑定地址信息
19 CHECK_RET(lst_sock.Listen());//开始监听
20 while(1){
21 TcpSocket cli_sock;
22 std::string cli_ip;
23 uint16_t cli_port;
24 //accept类成员函数,使用的私有成员_sockfd就是lst_sock的私有成员
25 cli_sock取地址传入,目的是为了获取accept接口返回的通信套接字描述符
26 bool ret=lst_sock.Accept(&cli_sock,&cli_ip,&cli_port);//获取新套接字
27 if(ret==false){
28 //获取新连接失败,可以重新继续获取下一个
29 continue;
30 }
31 std::string buf;
32 if( cli_sock.Recv(&buf)==false){
33
34 cli_sock.Close();//通信套接字接收数据出错,关闭的是通信套接字
35 continue;
36 }
37 printf("client:[%s:%d] say:%s\n",&cli_ip[0],cli_port,&buf[0]);
38 std::cout<<"server say:";
39 fflush(stdout);
40 buf.clear();
41 std::cin>>buf;
42 if(cli_sock.Send(buf)==false){
43 cli_sock.Close();
44 continue;
45 }
46 }
47 lst_sock.Close();
48 return 0;
49 }
~
为什么TCP服务端只能与一个客户端通信一次,不能持续与客户端通信?--流程阻塞
1.服务端程序阻塞位置:accept获取新连接处
2.服务端程序阻塞位置:与客户端通信处recv/send,接收缓冲区无数据则recv阻塞解决方案
while(1){
1.获取新建连接---accept接口是一个阻塞函数--若没有新连接则阻塞等待
2.通过新建连接与客户端进行通信
ssize_t ret=recv();--接收数据
ssize_t ret=send();--发送数据
}
举个例子说明流程阻塞的过程:
为什么TCP服务端只能与一个客户端通信一次,不能持续与客户端通信??的解决方案:
要防止流程阻塞,就要保证一个执行流只负责一个功能,一个执行流只管获取新连接,当新连接获取成功,然后创建新的执行流与客户端进行通信。多执行流的解决方案(多进程和多线程)
多进程:更加稳定
1.父进程创建子进程,数据独有,各自有一份cli_sock;然而子进程通过cli_sock通信,但是父进程不需要,因此父进程关闭自己的cli_sock;
2.父进程要等待子进程退出,避免产生僵尸进程;为了父进程只负责获取新连接,因此对于SIGCHLD信号自定义处理回调等待。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号init进程
僵尸进程:子进程先于父进程退出,操作系统在子进程退出时向父进程发送SIGCHLD信号通知父进程,你的子进程退出,处理方式是忽略,因此父进程没有关注子进程的退出状态,子进程成为僵
1 #include <iostream>
2 #include <stdlib.h>
3 #include "tcpsocket.hpp"
4 #include <signal.h>
5 #include <sys/wait.h>
6
7 //使用封装的Tcpsocket类实例化对象实现tcp服务端程序
8
W> 9 void sigcb(int signo){
10 //当子进程退出的时候就会像父进程发送sigchld信号,回调这个函数,
11 //waitpid返回值>0表示处理了一个退出的子进程
12 //waitpid<=0表示没有退出的子进程了
13 while(waitpid(-1,0,WNOHANG)>0);//一次回调循环将退出的子进程全部处理
14 }
15 int main(int argc,char *argv[]){
16 if(argc!=3){//表示参数的个数,看输入的参数是否为3
17 printf("em: ./tcp_srv 192.168.122.132 9000\n");
18 return -1;
19 }
20 std::string ip=argv[1];
21 uint16_t port =atoi(argv[2]);//stoi将字符串转换为数字
22
23 signal(SIGCHLD,sigcb);//有子进程退出就会收到SIGCHLD,回调sigcb
24 TcpSocket lst_sock;
25 CHECK_RET(lst_sock.Socket());//创建套接字
26 CHECK_RET(lst_sock.Bind(ip,port));//绑定地址信息
27 CHECK_RET(lst_sock.Listen());//开始监听
28 while(1){
29 TcpSocket cli_sock;
30 std::string cli_ip;
31 uint16_t cli_port;
32 bool ret=lst_sock.Accept(&cli_sock,&cli_ip,&cli_port);//获取新套接字
33 if(ret==false){
34 continue;
35 }
36 printf("new connnect:[%s:%d]\n",cli_ip.c_str(),cli_port);
37 //---------------------------------------------------------------
38 pid_t pid=fork();
39 if(pid==0){//子进程复制父进程--数据都有--代码共享
40
41 //让子进程处理与客户端通信
42 while(1){
43 std::string buf;
44 if( cli_sock.Recv(&buf)==false){
45
46 cli_sock.Close();//通信套接字接收数据出错,关闭的是通信套接字
47 exit(0);
48 }
49 printf("client:[%s:%d] say:%s\n",&cli_ip[0],cli_port,&buf[0]);
50 std::cout<<"server say:";
51 fflush(stdout);
52 buf.clear();
53 std::cin>>buf;
54 if(cli_sock.Send(buf)==false){
55 cli_sock.Close();
56 exit(0);
57 }
58 }
59 cli_sock.Close();
60 exit(0);
61 }
62 //父子进程数据独有,都会具有cli_sock,但是父进程并不通信
63 cli_sock.Close();//这个关闭对子进程没有影响,因为数据各自都有一份,避免资源泄露
64 wait(NULL);//等待子进程退出,防止出现僵尸进程
65 }
66 lst_sock.Close();
67 return 0;
68 }
尸进程。需要父进程等待子进程,获取返回值,释放子进程资源。
多线程:更加灵活,(线程使用成本低,资源、调度、创建销毁成本)
1.主线程获取到新连接然后创建新线程与客户端进行通信,但是需要将套接字描述符传入线程执行函数中
2.但是传入这个描述符的时候,不能使用局部变量的地址传递(局部变量的空间在循环完毕就会被释放),可以传描述符的值,也可传入new的对象
3.c++中对于类型强转,将数据值当作指针传递有很多限制,想办法克服。
4.主线程中虽然不适用cli_sock,但是不能关闭cli_sock,因为线程间共享资源,一个线程释放,另一个线程也就没法使用。
cli_sock是一个TcpSocket类实例化的对象,_sock_fd是cli_sock中的一个成员变量--用于保存创建套接字返回的文件描述符
cli_sock.Recv(buf)接收数据,Recv内部通过cli_sock的sockfd接收数据
1 #include <iostream>
2 #include <stdlib.h>
3 #include "tcpsocket.hpp"
4 #include <sys/wait.h>
5 #include <pthread.h>
6
7 //使用封装的Tcpsocket类实例化对象实现tcp服务端程序
8
9 void *thr_start(void *arg){
10 long fd=(long)arg;
11 TcpSocket cli_sock;
12 cli_sock.SetFd(fd);
13 while(1){
14 std::string buf;
15 if( cli_sock.Recv(&buf)==false){
16
17 cli_sock.Close();//通信套接字接收数据出错,关闭的是通信套接字
18 pthread_exit(NULL);//线程退出,exit(0)进程退出
19 }
20 printf("client:say:%s\n",&buf[0]);
21 std::cout<<"server say:";
22 fflush(stdout);
23 buf.clear();
24 std::cin>>buf;
25 if(cli_sock.Send(buf)==false){
26 cli_sock.Close();
27 pthread_exit(NULL);
28 }
29 }
30 return NULL;
31 cli_sock.Close();
32 }
33 int main(int argc,char *argv[]){
34 if(argc!=3){//表示参数的个数,看输入的参数是否为3
35 printf("em: ./tcp_srv 192.168.122.132 9000\n");
36 return -1;
37 }
38 std::string ip=argv[1];
39 uint16_t port =atoi(argv[2]);//stoi将字符串转换为数字
40
41 TcpSocket lst_sock;
42 CHECK_RET(lst_sock.Socket());//创建套接字
43 CHECK_RET(lst_sock.Bind(ip,port));//绑定地址信息
44 CHECK_RET(lst_sock.Listen());//开始监听
45 while(1){
46 TcpSocket cli_sock;
47 std::string cli_ip;
48 uint16_t cli_port;
49 //accept类成员函数,使用的私有成员_sockfd就是lst_sock的私有成员
50 cli_sock取地址传入,目的是为了获取accept接口返回的通信套接字描述符
51 bool ret=lst_sock.Accept(&cli_sock,&cli_ip,&cli_port);//获取新套接字
52 if(ret==false){
53 //获取新连接失败,可以重新继续获取下一个
54 continue;
55 }
56 printf("new connnect:[%s:%d]\n",cli_ip.c_str(),cli_port);
57 //----------------------------------------------------------------------
58 pthread_t tid;
59 //将通信套接字当作参数传递给线程,让线程与客户端进行通信
60 //cli_sock是一个局部变量--循环完了这个资源就会被释放
61 pthread_create(&tid,NULL,thr_start,(void*)cli_sock.GetFd());//创建线程
62 pthread_detach(tid);//不关心线程返回值,分离线程,然后退出后自动释放资源
63 //主线程不能关闭cli_sock套接字,因为多线程是共用描述符的
64 }
65 lst_sock.Close();
66 return 0;
67 }
连接断开在发送端和接收端上的表现(和管道读写特性一样):
1.接收端:连接断开,则recv返回0;反之若recv返回0,表示连接断开(套接字写端被关闭--双工通信)
2.发送端:连接断开,则send触发异常导致进程退出
管道:
管道所有读端关闭,继续写入就会触发SIGPIPE异常
管道所有写端关闭,继续读取返回0
总结:
1.udp编程流程及接口:
客户端:socket/sendto/recvfrom/close
服务端:socket/bind/recvfrom/sendto/close
2.tcp编程流程及接口:
客户端:socket/connect/send/recv/close
服务端:socket/bind/listen/accept/recv/send/close
3.字节序转换接口:
htons/ntohs/htonl/ntohl/inet_addr/inet_ntoa/inet_ntop/inet_pton
4.多执行流tcp服务端流程
多执行流思想(多进程、多线程)
|