UDP(User Datagram Protocol 用户数据报协议,是不可靠的数据报传输协议,不确保数据安全有序的到达对端。
特点:
应用场景:性能要求大于安全要求,比如视频传输。
就是网络通信程序的编写,网络中的各种通信,都是用户与服务器之间的通信,不存在用户与用户之间的直接通信。也不存在服务器和服务器之间的直接通信。
客户端:网络通信中用户的一端,是进行业务请求的一端,是主动发起请求的一端。
服务端:网络通信中提供服务的一端,针对客户端请求进行处理的一端,是被动接收请求的一端。
客户端要给服务端发送数据,客户端怎么知道要发送给谁呢?
五元组标识了一条通信:数据从哪来,到哪去,用的什么协议。
创建套接字
int socket(int domain, int type, int protocol);
domain:地址域类型,用于决定通信使用什么地址结构,IPV4地址域类型则是AF_INET。
type:套接字类型,决定使用什么套接字传输方式。
protocol:使用的协议类型,流式套接字默认0则表示TCP协议,数据报套接字默认0则表示UDP协议。
返回值:套接字操作句柄(文件描述符),失败返回-1。
绑定地址信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:创建套接字返回的操作句柄,用于决定给哪个套接字绑定地址。
addr:绑定地址的信息。
len:地址信息长度。
**注意:**struct sockaddr是一个通用的地址接口,在真正使用的时候并不会使用它,而是使用一个具体的通信地址结构,然后强转其类型并传入数据即可,bind接口内部会根据传入数据的前2个字节决定这个传入的地址数据该如何解析。len是地址信息长度,作用是第二个参数传入的是地址,所以要指定访问的长度,防止访问越界。
返回值:成功返回0,失败返回-1.
发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:创建套接字返回的操作句柄。
buf:要发送的数据的首地址(socket并不关心发送数据的具体内容)。
len:要发送的数据长度。
flags:标志位,0是默认阻塞操作。
dest_addr:目的端地址信息。
addrlen:地址信息长度。
返回值:成功返回实际发送的数据长度,失败返回-1。
接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:创建套接字返回的操作句柄。
buf:一块缓冲区的首地址,用于存放获取的数据。
len:想要发送的数据长度。
flags:标志位,0是默认阻塞操作。(若缓冲区没有数据则会等待)
src_addr:接收到的数据的源端地址信息。
*addrlen:输入输出参数,用于指定要获取的地址长度,以及返回实际长度。
返回值:成功返回获取到的数据长度,失败返回-1。
关闭套接字
int close(int fd);
字节序转换接口
网络通信需要使用网络字节序,因此要考虑网络字节序转换问题
16位数据的主机与网络字节序转换:uint16_t htons(uint16_t hostshort); uint16_t ntohs(uint16_t netshort);
32位数据的主机与网络字节序转换:uint32_t htonl(uint32_t hostlong); uint32_t ntohl(uint32_t netlong);
将一个点分十进制的字符串IP地址转换为网络字节序整数IP地址:in_addr_t inet_addr(const char *cp);
将一个网络字节序整数IP地址转换为点分十进制的字符串IP地址:char *inet_ntoa(struct in_addr in);
服务器端:
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
//从参数中获取服务端要绑定的地址:包括IP地址和端口号
if(argc < 3)
{
printf("参数不全\n");
return -1;
}
char* src_ip = argv[1];//获取要绑定的IP地址
int srv_port = atoi(argv[2]);//获取要绑定的端口
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET表示IPV4地址域类型;SOCK_DGRAM表示数据报套接字,0默认是使用UDP协议
if(sockfd < 0)
{
perror("socket error");
return -1;
}
//2.绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(srv_port);
addr.sin_addr.s_addr = inet_addr(src_ip);
socklen_t len = sizeof(addr);
int ret = bind(sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return -1;
}
//3.接收数据
while(1)
{
char tmp[4096] = {0};
struct sockaddr_in client_addr;//获取发送端的地址信息
len = sizeof(client_addr);
ret = recvfrom(sockfd, tmp, 4095, 0, (struct sockaddr*)&client_addr, &len);
if(ret < 0)
{
perror("recvfrom error");
return -1;
}
printf("%s:%d - %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), tmp);
//4.发送数据
printf("server input:");
fflush(stdout);
memset(tmp, 0x00,4096);//清空缓冲区内容
scanf("%s",tmp);
ret = sendto(sockfd, tmp, strlen(tmp), 0, (struct sockaddr*)&client_addr, len);
}
//5.关闭套接字
close(sockfd);
return 0;
}
客户端:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class UdpSocket
{
private:
int _sockfd;
public:
UdpSocket()
:_sockfd(-1)
{}
public:
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const string &ip, int port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
if(bind(_sockfd, (struct sockaddr*)&addr, len) < 0)
{
perror("bind error");
return false;
}
return true;
}
//发送数据
bool Send(const string &data, const string &ip, int port)
{
struct sockaddr_in peeraddr;
peeraddr.sin_family = AF_INET;
peeraddr.sin_port = htons(port);
peeraddr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, &data[0], data.size(), 0, (struct sockaddr*)&peeraddr, len);
if(ret < 0)
{
perror("send error");
return false;
}
return true;
}
//客户端实际上是不需要接收服务器的地址的,因为他本来就知道
bool Recv(string *buf, string *ip = nullptr, int* port = nullptr)
{
struct sockaddr_in peeraddr;
socklen_t len = sizeof(sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4095, 0, (struct sockaddr*)&peeraddr, &len);
if(ret < 0)
{
perror("recvfrom error");
return false;
}
buf->assign(tmp,ret);//从tmp字符串位置开始截取ret长度的数据到buf中
if(ip != nullptr)
{
*ip = inet_ntoa(peeraddr.sin_addr);
}
if(port != nullptr)
{
*port = ntohs(peeraddr.sin_port);
}
return true;
}
//关闭套接字
bool Close()
{
return close(_sockfd);
}
};
#include"udpsocket.hpp"
#include
using namespace std;
int main(int argc, char* argv[])
{
//通过运行参数获取服务端的地址信息
if(argc < 3)
{
cout<<"参数不全"<<endl;
return -1;
}
string srv_ip = argv[1];
int srv_port = atoi(argv[2]);
//1.创建套接字
UdpSocket sock;
sock.Socket();
//2.绑定地址信息(不推荐)
while(1)
{
string buf;
cout << "请输入要发送的内容";
cin >> buf;
//3.发送请求
sock.Send(buf, srv_ip, srv_port);
//4.接收相应
buf.clear();
sock.Recv(&buf);
cout << buf << endl;
}
//5.关闭套接字
sock.Close();
return 0;
}
TCP(Transmission Control Protocol 传输控制协议),是面向连接的,可靠的字节流传输协议(确保了数据安全有序的到达对端,并且建立连接后才可以进行通信),TCP协议为了保证可靠传输,因此使用了很多的机制来完成,因此传输性能相对于UDP协议来说较低。
特点
应用场景:安全需求大于性能需求,比如文件传输。
服务端开始监听
int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:当前服务器在同一时间所能处理的最大的客户端连接请求数量(同一时刻的最大并发连接数)。
SYN泛洪攻击:恶意主机伪造IP地址,向服务器发送大量的连接请求,这样服务端就会不断创建新的连接套接字,如果服务端对新建套接字的数量不做限制的话,有可能瞬间资源耗尽,系统崩溃。这个限制就是backlog,有了这个限制,遇到SYN泛洪攻击的时候,顶多是无法处理正常的请求,但是不会让系统崩溃,之前的连接还可以正常通信。
返回值:成功返回0,失败返回-1。
客户端发送连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:服务端的地址信息,IPV4通信使用struct sockaddr_in的结构。
len:地址信息长度。
返回值:成功返回0,失败返回-1。
服务端获取新建连接句柄
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:监听套接字描述符(监听套接字描述符仅用来进行新建连接和监听)。
addr:获取要进行连接的客户端地址信息,描述的是当前要获取的这个套接字是与哪个客户端进行通信的。
len:输入输出型参数:指定要获取的地址长度,以及返回实际获取的地址长度。
返回值:成功返回新建连接的套接字描述符,用于后续与客户端进行通信;失败返回-1。
发送数据:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符,对于服务端来说,一定是accept获取到的新建连接的套接字描述符。
buf:要发送的数据首地址。
len:要发送的数据首地址。
flag:标志位,通常置0,表示阻塞发送,就是把数据放到发送缓冲区,系统进行封装发送,如果缓冲区满了则进行等待。
返回值:成功返回实际发送的数据的长度;失败返回-1。
接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:监听套接字描述符。
buf:一个缓冲区空间首地址,用于存放接收的数据。
len:想要获取的数据长度,不能大于buf的缓冲区长度。
flag:标志位,通常置0,表示阻塞接收,就是socket接收缓冲区中如果没有数据则阻塞。
返回值:成功返回实际获取到的数据长度;失败返回-1;连接断开返回0。
关闭套接字
int close(int fd);
部分关闭连接
int shutdown(int sockfd, int how); 这个操作并不会完全释放资源。
shutdown更多用于进行半关闭连接,让对方知道自己不再发送数据或者不再接收数据了,但是要注意shutdown不是用于关闭套接字释放资源的,就算调用了shutdown,最后也必须使用close关闭释放资源。
封装TCPSocket类
#include
#include
#include
#include
#include
#include
#define MAX_LISTEN 5
using namespace std;
class TCPSocket
{
private:
int _sockfd;
public:
TCPSocket()
:_sockfd(-1)
{}
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const string &ip, int port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(addr);
if(bind(_sockfd, (struct sockaddr*)&addr, len) < 0)
{
perror("bind error");
return false;
}
return true;
}
//服务端开始监听
bool Listen(int backlog = MAX_LISTEN)
{
if(listen(_sockfd, backlog) < 0)
{
perror("listen error");
return false;
}
return true;
}
//向服务端发起连接请求
bool Connect(const string &srv_ip, int srv_port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(srv_port);
addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
socklen_t len = sizeof(addr);
if(connect(_sockfd, (struct sockaddr*)&addr, len) < 0)
{
perror("connect error");
return false;
}
return true;
}
//获取新建连接
bool Accept(TCPSocket* new_sock, string* cli_ip, int* cli_port)
{
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if(new_fd < 0)
{
perror("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if(cli_ip != nullptr)
{
*cli_ip = inet_ntoa(addr.sin_addr);
}
if(cli_port != nullptr)
{
*cli_port = ntohs(addr.sin_port);
}
return true;
}
bool Send(const string &data)
{
ssize_t ret = send(_sockfd, data.c_str(), data.size(), 0);
if(ret < 0)
{
perror("send error");
return false;
}
return true;
}
bool Recv(string *buf)
{
char tmp[4096] = {0};
ssize_t ret = recv(_sockfd, tmp, 4096, 0);
if(ret < 0)
{
perror("recv error");
return false;
}
else if(ret == 0)
{
cout<<"连接断开"<<endl;
}
buf->assign(tmp,ret);
return true;
}
bool Close()
{
if(_sockfd > 0)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
};
客户端
#include "TCPSocket.hpp"
int main(int argc, char* argv[])
{
if(argc < 3)
{
cout<<"参数不全"<<endl;
return -1;
}
string srv_ip = argv[1];
int srv_port = stoi(argv[2]);
TCPSocket sock;
//1.创建套接字
sock.Socket();
//2.向服务端发起连接请求
sock.Connect(srv_ip, srv_port);
//3、循环收发数据
while(1)
{
string data;
cout<<"clint input:";
fflush(stdout);
cin>>data;
sock.Send(data);
data.clear();
sock.Recv(&data);
cout<<"server response"<<data<<endl;
}
//4.关闭套接字
sock.Close();
return 0;
}
服务端
#include "TCPSocket.hpp"
#include
unordered_map<string, string> table =
{
{"hello", "你好"},
{"goodmorning", "早上好"}
};
int main()
{
TCPSocket listen_sock;
//1.创建套接字
listen_sock.Socket();
//2.绑定地址信息
listen_sock.Bind("0.0.0.0", 9000);
//3.开始监听
listen_sock.Listen();
//5.使用新建连接收发数据
while(1)
{
//4.获取新建连接
TCPSocket new_sock;
string cli_ip;
int cli_port;
listen_sock.Accept(&new_sock, &cli_ip, &cli_port);
cout<<"new connect"<<cli_ip<<":"<<cli_port<<endl;
string buf;
new_sock.Recv(&buf);
string rsp;
auto it = table.find(buf);
if(it == table.end())
{
rsp = "未知请求";
}
rsp = it->second;
new_sock.Send(rsp);
}
//6.关闭套接字
listen_sock.Close();
return 0;
}