- 通信中两端主机:客户端和服务端。
- 服务端:被动接受请求的一端。
- 客户端:主动发送请求的一端。
- 永远都是客户端主机先向服务端发送请求。
- 传输层有两个协议:tcp、udp
一、socket套接字编程
- socket是一套网络编程接口,类似于中间件;上层用户可以通过这些接口简单的完成网络通信传输;而不需要过于关心内部的实现过程。
- 套接字编程讲的就是使用socket接口实现网络通信。
- socket编程:
① UDP协议:用户数据报协议。特性:无连接、不可靠、面向数据报。常用于对实时性要求高于安全性要求的程序。如视频传输。
② TCP协议:传输控制协议。特性:面向连接、可靠传输、面向字节流。常用于对安全可靠性要求高于对性能要求的程序。如文件传输。
③ 面向数据报:无连接、不可靠的、无序的,有最大长度限制的数据传输服务。
④ 面向字节流:基于连接的、可靠的、有序的,双向的字节流传输服务,不限制上层传输数据大小的传输方式。
1.socket接口的介绍
① 创建套接字
int socket(int domain, int type, int protocol);
domain:地址域—不同的网络地址结构,AF_INET - IPv4地址域。
type:套接字类型 - 流式套接字 / 数据报套接字。(流式套接字:一种有序的,可靠的,双向的,基于连接的字节流传输 SOCK_STREAM;数据报套接字:无连接的,不可靠的,有最大长度限制的传输。 SOCK_DGRAM)
protocol:使用的协议 0—不同套接字类型下的默认协议(流式套接字默认的是tcp/数据报套接字默认的是udp)IPPROTO_TCP----tcp协议 。 IPPRORO_UDP----udp协议。
返回值:返回套接字的操作句柄----文件描述符
② 为套接字绑定地址信息
int bind(int sockfd, 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(sockaddt->sin_family == AF_INET6)
}
③ 发送数据
int sendto(int sockfd, char* data, int data_len, int fag, struct sockaddr* dest_addr, socklen_t addr_len);
sockfd:套接字操作句柄,发送数据就是将数据拷贝到内核的socket发送缓冲区中
data:要发送的数据的首地址
data_len:要发送的数据的长度
flag:选项参数,默认为0—表示当前操作是阻塞操作 MSG_DONTWAIT—设置为非阻塞。若发送数据的时候,socket发送缓冲区已经满了,则0默认阻塞等待; MSG_DONTWAIT就是立即报错返回了。
dest_addr:目的端地址信息结构–表示数据要发送给谁。每一条数据都要描述源端信息(绑定的地址信息)和对端信息(当前赋予的信息)。
④ 接收数据
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。
⑤ 关闭套接字
int close(int fd);
2.IP地址的三种表示方式以及字节序转换接口
(1.)IP地址的三种表示方式
- 点分十进制
点分十进制是IPv4的IP地址标识方法。四个字节表示一个IP地址,每个字节按照十进制表示0~255。
如192.168.0.1 - 主机字节序
主机字节序是指按照处理器的存储数据的字节顺序,一般Intel处理器是小端字节序
小端字节序:低地址存放在低字节位,高地址存高字节位。如0x12345678====》0x 78 56 34 12
故Intel处理器的主机字节序一般为小端字节序。 - 网络字节序
网络字节序是TCP/IP规定好的存储数据的格式,为大端字节序
大端字节序:低地址存放高字节位,高地址存放低字节位。如0x12345678====》0x 12 34 56 78
(2)字节序转换接口
接口名 | 含义 |
---|---|
uint32_t htonl(uint32_t) ; | 将4个字节的转换成网络字节序 |
uint32_t ntohl(uint32_t); | 将4个字节网咯字节序数据转换为主机字节序 |
uint16_t htons(uint16_t); | 将2个字节的主机字节序转换成网络字节序 |
uint16_t ntohs(uint16_t); | 将2个字节网络字节序转换成主机字节序 |
uint32_t inet_addr(const char* ip); | 将字符串IP地址转换为网络字节序整数IP地址 |
int inet_pton(int domain, char* src, void* dst); | 将字符串src的IP地址按照domain地址域网络字节序的IP地址 |
int inet_ntop(int domain, void* src, char* dst, int len); | 将网络字节序数据按照domain地址域转换为字符串IP地址 |
二、UDP网络编程流程的实现
- 特性:无连接,不可靠,面向数据报。
1.UDP网络通信程序编程流程:
- 服务端:
① 创建套接字socket。在内核中创建一个socket结构体,通过这个结构体以及返回的文件描述符,使进程与网卡之间建立关联。 服务端socket只能绑定的是服务端主机上的IP地址;客户端也绑定的是自己主机的IP地址。
② 为套接字绑定地址信息。在套接字结构体中标记地址信息,让操作系统知道发送数据的时候通过哪些地址发送,以及接受数据的时候,知道应该由哪一个进程来处理。
③ 数据接收,数据的发送。发送数据的时候层层封装数据(网络的传输数据都应该包含:源IP地址/目的IP地址/源端口/目的端口/协议)。操作系统对于接收到的数据,判断目的地址信息,去内核中socket链表—查找(找到了,则将这个数据放入了这个socket的接受缓冲区中,没找到,则这个数据直接被丢弃。 )
④ 关闭套接字,释放资源。 - 注意事项:
① 客户端不推荐主动绑定地址,而是在发送数据的时候由操作系统选择合适的地址信息进行绑定(尽最大的可能避免出现端口冲突概率。)
② 服务端必须主动绑定,因为需要保证地址永远不改变才能被客户端找到。
2.udpsocket类实现套接字编程
//UDPSocket.hpp
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unisted.h>
#include <string>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace sheena
{
class UdpSocket
{
private:
void MakeAddr(struct sockaddr_in& addr, const std::string& ip, const uint16_t port)
{
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
}
public:
UdpSocket()
:_sock(-1)
{}
~UdpSocket()
{}
//1.创建套接字
bool Socket()
{
//AF_INET:IPv4地址域;SOCK_DGRAM:用户数据报套接字;IPPROTO_UDP:udp协议
_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//创建失败返回-1
if(_sock < 0)
{
perror("创建失败!!\n");
return false;
}
return true;
}
//2.绑定地址信息
bool Bind(const std::string &ip, const uint16_t port)
{
struct sockaddr_in addr;
MakeAddr(addr, ip, port);
socklen_t len = sizeof(struct sockaddr_in);
//sockfd:套接字描述符,addr:地址信息(IPv4是struct addr_in结构体)
int ret = bind(_sock, (struct sockaddr*)& addr, len);
if(ret < 0)
{
perror("绑定地址信息失败!!!\n");
return false;
}
return true;
}
//3.接收信息
bool Recv(std::string& buf, std::string *ip = NULL, uint16_t *port = NULL)
{
struct sockaddr_in addr;
char tmp[1500] = {0};
socklen_t len = sizeof(struct sockaddr_in);
//sockfd:套接字描述符,buf:接收信息的缓冲区,len:要接收数据的长度,flag:参数为0为阻塞。src_addr:对端地址信息
int ret = recvfrom(_sock, tmp, 1500, 0, (struct sockaddr*)& addr, &len);
if(ret < 0)
{
perror("接收信息失败!!!\n");
return false;
}
buf.assign(tmp, ret);
if(ip != NULL)
{
//将网络字节序整数IP地址转换为字符串IP
*ip = inet_ntoa(addr.sin_addr);
}
if(*port != NULL)
{
//将网络字节序转换为主机字节序
*port = ntohs(addr.sin_port);
}
return true;
}
//4.发送信息
bool send(const std::string &buf, std::string& dip, uint16_t dport)
{
struct sockaddr_in addr;
MakeAddr(addr, dip, dport);
socklen_t len = sizeof(struct sockaddr_in);
//sockfd:套接字描述符,buf:要发送的数据缓冲区,len:要发送的数据长度,
//flag :参数为0位阻塞发送,dest_addr:要发送地址信息
int ret - sendto(_sock, &buf[0], buf, size(), 0, (struct sockaddr*)& addr, len);
if(ret < 0)
{
perror("发送信息失败!!\n");
return false;
}
return true;
}
//5.关闭套接字
bool Close()
{
close(_sock);
return true;
}
private:
int _sock;
};
}
3.udp客户端程序
- 对于客户端来说,并不关心使用什么源端地址将地址发送出去,只要能够发送数据并且能接收数据就可以。
- 服务端必须主动绑定地址。---- 一旦服务端不主动绑定地址,则会造成操作系统随意选择合适的地址进行绑定,服务端自己都不确定自己用什么地址。如何告诉客户端。因此服务端通常必须主动绑定地址,并且不能随意改动。
#include <stdio.h>
#include <iostream>
#include <string>
#include "UdpSocket.hpp"
#define CHCK_RET(q) if((q) == false) {return -1;}
namespace sheena
{
//客户端要给服务端发送数据,那么就需要知道服务端的地址信息
//因此通过程序运行参数传入服务端的地址信息
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cerr << "./udp_client ip port\n";
return -1;
}
//这两个地址信息是服务端地址信息,表示数据由客户端发给谁
std::string ip_addr = argv[1];//服务端地址信息
uint1_t port_addr = atoi(9000);
UdpSocket scok;
CHECK_RET(sock.Socket());
//CHECK_RET(sock.Bind());因为客户端不知道绑定的是什么地址信息,因为传入的地址信息是服务端的地址信息(对端)
//所以不绑定地址信息
//客户端通常并不主动绑定地址,而是在发送数据的时候,操作系统检测有没有
//绑定,则会自动选择一个合适的地址和端口进行绑定
while(1)
{
std::cout << "client say:" ;
fflush(stdout);
std::string buf;
std::cin >> buf;
sock.Send(buf, srv_ip, srv_port);
buf.clear();
sock.Recv(buf);
std::cout << "server say: " << buf << endl;
}
sock.Close();
std::cout << "Hello world" << std::endl;
return 0;
}
}
4.udp服务端程序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
namespace sheena
{
int main(int argc, char* argv)
{
//通过参数灵活获取IP地址和端口信息
if(argc != 3)
{
printf("./udp_srv is port\n");
return -1;
}
//获取本地主机端口号和主机地址
uint16_t port = atoi(argv[2]);
char* ip = argv[1];
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGREAM, IPPROTO_UDP);
if(sockfd < 0)
{
perror("创建套接字失败!!\n");
return -1;
}
//2.绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
socklen_t len = sizeof(addr);
int ret = bind(sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("绑定地址信息失败!!!\n");
return -1;
}
while(1)
{
//3.接收信息
char buf[1024] = {0};
struct sockaddr_in cliaddr;
ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cliaddr, &len);
if(ret < 0)
{
perror("接收信息失败!!\n");
return -1;
}
printf("client say: %s\n", buf);
//4.发送信息
printf("server say: ");
fflush(stdout);
memset(buf, 0x00, 1024);
scanf("%s",buf);
ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockadrr*)& cliaddr, len);
if(ret < 0)
{
perror("发送信息失败!!!\n");
return -1;
}
}
//5.关闭套接字
close(sockfd);
return 0;
}
}
- 每个程序绑定的都是自己的网卡地址信息。
- 客户端发送的对端地址信息一定是服务端绑定的地址信息。
- 服务端绑定的地址信息,一定是自己主机上的网卡IP地址。
三、TCP网络编程流程的实现
- 特性:面向连接、可靠传输、面向字节流
1.TCP网络通信程序编程流程
- 客户端:创建套接字,描述地址信息,发送连接请求,连接建立成功,收发数据,关闭套接字。
- 服务端:创建套接字,描述地址信息,开始监听,接收连接请求,新建套接字,获取新建套接字描述符,通过这个描述符与客户端通信,关闭套接字。
2.接口介绍
① 创建套接字:int socket(int domain, int type, int protocol); (AF_INET,SOCK_STREAM—流式套接字,IPPROTO_TCP)
② 绑定地址信息:int bind(int sockfd, struct sockaddr* addr, socklen_t len);
③ 服务端开始监听:int listen(int sockfd, iny backlog);告诉操作系统开始接收连接请求。
backlog:决定同一时间,服务端能接收的客户端连接请求数量。(SYN泛洪攻击:恶意主机不断向服务端主机发送大量的连接请求,若服务端为每一个连接请求建立socket,则会瞬间资源耗尽,服务器崩溃。因此服务端有一个connection pending queue; 存放为连接请求新建的socket节点;backlog参数决定了这个队列的最大节点数量;若这个队列放满了,若还有新连接请求的到来,则将这个后续请求丢弃掉。)但是这个并不能决定服务端能够接收多少客户端请求数量。
④ 获取新建socket的操作句柄:从内核指定socket的pending queue中取出一个socket,返回操作句柄。
int accept(int sockfd, struct sockaddr* addr, socklen_t* len);
sockfd:监听套接字—指定要获取哪个pending queue中的套接字。
addr:获取一个套接字,这个套接字与指定的客户端进行通信,通过addr获取这个客户端的地址信息。
len:输入输出型参数----指定地址信息想要的长度,以及返回实际的地址长度。
返回值:成功则返回新获取的套接字描述符----操作句柄。失败则返回-1。
⑤ 通过重新获取的套接字操作句柄(accept返回的描述符)与指定客户端进行通信
接收数据:ssize_t recv(int sockfd, char* buf, int len, int flag);返回值:成功返回实际读取的数据长度,连接断开返回0,读取失败返回-1。(tcp连接断开也是写端被关闭的一种体现)
发送数据:ssize_t send(int sockfd, char* data, int len, int flag);返回值:成功返回实际发送的数据长度,失败返回-1,若连接断开则触发异常。
⑥ 关闭套接字:释放资源。int close(int fd);
⑦ 客户端向服务端发送连接请求:int connect(int sockfd, int sockaddr* addr, socklen_t lem);
sockfd:客户端套接字—若还未绑定地址,则操作系统会选择合适的源端地址进行绑定。
addr:服务端地址信息—struct sockaddr_in:这个地址信息经过connect之后也会描述到socket中。
len:地址信息长度。
- 注意:连接若是断开了,recv会返回0,send会触发异常导致进程退出。
3.tcpsocket类实现套接字编程
#ifndef __MY_TCP_H
#define __MY_TCP_H
#include <iostream>
#include <string>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define CHECK_RET(q) if((q) == false){return -1;}
#define MAX_LISTEN 10
namespace sheena
{
class TcpSocket
{
private:
void MakeAddr(struct sockaddr_in& addr, const std::string& ip, const uint16_t port)
{
//添加地址信息
addr.sin_family = AF_INET;
addr,sin_port = port;
addr.sin_adrr.s_addr = inet_addr(ip.c_str());
}
public:
TcpSocket()
:_sockfd(-1)
{}
//1.创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0)
{
perror("创建套接字失败!!\n");
return false;
}
return true;
}
//2.绑定地址信息
bool Bind(const std::string& ip, const uint16_t port)
{
struct sockaddr_in addr;
MakeAddr(add, ip, port);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)& addr, len);
if(ret < 0)
{
perror("绑定地址信息!!\n");
return false;
}
return true;
}
//3.开始监听(服务器)
bool Listen(int backlog = MAX_LISTEN)
{
//backlog:决定了同一时间并发连接数,不能写死,采用默认参数方式
int ret = listen(_sockfd, backlog);
if(ret < 0)
{
perror("监听失败!!\n");
return false;
}
return true;
}
//3.发起连接请求(客户端)
bool Connect(const std::string& srvip, const uint16_t port)
{
struct sockaddr_in addr;
MakeAddr(addr, srvip, port);
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("发起连接请求失败!!!\n");
return false;
}
return true;
}
//4.获取新连接(服务器)
bool Accept(TcpSocket* sock, std::string* ip = NULL, uint16_t *port = NULL)
{
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
//获取新连接的套接字描述符,并且获取这个新连接对应的客户端地址
int newfd = accept(_sockfd, (struct sockaddr*)& addr, &len);
if(newfd < 0)
{
perror("获取新连接失败!!!\n");
return false;
}
sock->_sockfd = newfd;
if(ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if(port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
//5.接收信息
bool Recv(std::string& buf)
{
char tmp[4096] = {0};
int rlen = recv(_sockfd, tmp, 4096, 0);
if(rlen < 0)
{
//连接失败
perror("接收信息失败!!\n");
return false;
}
else if(rlen == 0)
{
//连接断开
std::cerr << "连接已断开\n";
return false;
}
buf.assign(tmp, rlen);
return true;
}
//6.发送信息
bool Send(std::string& buf)
{
int ret = send(_sockfd, &buf[0], buf,size(), 0);
if(ret < 0)
{
perror("发送信息失败!!\n");
return false;
}
return true;
}
//7.关闭套接字
bool Close()
{
close(_sockfd);
return true;
}
private:
int _sockfd;
};
#endif
}
4.tcp客户端程序
#include <stdlib.h>
#include <stdio.h>
#include "TcpSocket.hpp"
namespace sheena
{
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cerr << "./tcp_cli ip port\n";
return -1;
}
std::string ip =argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket cli_sock;
//1.创建套接字
CHECK_RET(cli_sock.Socket());
//不推荐客户端主动绑定地址信息,需要系统自动绑定,所以不需要该步
//2.绑定地址信息
//CHECK_RET(cli_sock.Bind());
while(1)
{
//3.发送信息
std::cout << "client say: ";
fflush(stdout);
std::string buf;
std::cin >> buf;
CHECK_RET(cli_sock.Send(buf));
//4.接收信息
buf.clear();
CHECK_RET(cli_sock.Recv(buf));
std::cout << "server say: " << buf << "\n";
}
cli_sock.Close();
return 0;
}
}
5.tcp服务端程序
#include <stdlib.h>
#include <stdio.h>
#include "TcpSocket.hpp"
namespace sheena
{
int main(int argc, char* ardv[])
{
if(argc != 3)
{
std::cout << "./tcp_srv ip port\n";
return -1;
}
//通过参数获取地址信息
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
//实例化对象,监听套接字
TcpSocket lst_sock;
//1.创建套接字
CHECK_RET(lst_sock.Sock());
//2.绑定地址信息
CHECK_RET(lst_sock.Bind(ip, port));
//3.开始监听
CHECK_RET(lst_sock.Listen());
TcpSocket cli_sock;
//4.获取新连接
while(1)
{
std::string cli_ip;
uint16_t cli_port;
if(lst_sock.Accept(&cli_sock, &cli_ip, &cli_port) == false))
{
//获取新连接失败后,继续获取下一个连接
continue;
}
//获取新连接成功
std::cout << "新连接:" << cli_ip << ":" << cli_port << "\n";
//5.开始通信,接收信息
std::string buf;
bool ret = cli_sock.Recv(buf);
if(ret == false)
{
//和当前的客户端通信不能连接
//继续下一个客户端处理
cli_sock.Close();
continue;
}
std::cout << "client say: " << buf << "\n";
std::cout << "server say: ";
fflush(stdout);
buf.clear();
std::cin >> buf;
//6.发送信息
ret = cli_sock.Send(buf);
if(ret ==false)
{
cli_sock.Close();
continue;
}
}
lst_sock.Close();
return 0;
}
}