网络套接字编程
一、 认识UDP协议
UDP(User Datagram Protocol 用户数据报协议,是不可靠的数据报传输协议,不确保数据安全有序的到达对端。
特点:
应用场景:性能要求大于安全要求,比如视频传输。
二、UDP通信程序的编写
0. 前提
a. 什么是套接字编程
就是网络通信程序的编写,网络中的各种通信,都是用户与服务器之间的通信,不存在用户与用户之间的直接通信。也不存在服务器和服务器之间的直接通信。
b. 网络通信都是两端主机之间的通信:客户端,服务端。
客户端:网络通信中用户的一端,是进行业务请求的一端,是主动发起请求的一端。
服务端:网络通信中提供服务的一端,针对客户端请求进行处理的一端,是被动接收请求的一端。
客户端要给服务端发送数据,客户端怎么知道要发送给谁呢?
- 服务端都会提前将自己的地址信息封装在客户端中,也正是因为如此,客户端的地址信息通常都不能改变。
c. 网络传输的数据都会具有五元组
- sip:源端IP
- sport:源端端口
- dip:对端IP
- dport:对端端口
- protocol:协议
五元组标识了一条通信:数据从哪来,到哪去,用的什么协议。
1. 了解通信流程
a. 客户端
- 创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
- 绑定地址信息:不建议进行,因为一旦绑定了地址,发送数据的源端地址就是固定的,但是一个端口只能被一个进程占用,容易冲突。
- 发送数据:将数据放到发送缓冲区,并告诉套接字这些数据要发送给谁(对端地址),系统在封装的时候就会发现数据没有源端地址,这时操作系统就会自动选择一个合适的地址信息进行绑定。
- 接收数据:从套接字的接收缓冲区取出数据。
- 关闭套接字:释放资源。
b. 服务端
- 创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
- 为套接字绑定地址信息:描述sip和sport到socket中,用于告诉操作系统当收到数据时,这个数据要发送给所描述的sip和sport的时候,要交给这个socket进行处理。
- 接收数据:从套接字的接收缓冲区中取出数据,顺便获取这个数据是谁发的(源端地址)。
- 发送数据:将要发送的数据放入到发送缓冲区中,并且告诉套接字这个数据要发送给谁。
- 关闭套接字:释放资源。
2. 认识通信接口
-
创建套接字 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);
3. 编写程序
服务器端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("参数不全\n");
return -1;
}
char* src_ip = argv[1];
int srv_port = atoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket error");
return -1;
}
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;
}
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);
printf("server input:");
fflush(stdout);
memset(tmp, 0x00,4096);
scanf("%s",tmp);
ret = sendto(sockfd, tmp, strlen(tmp), 0, (struct sockaddr*)&client_addr, len);
}
close(sockfd);
return 0;
}
客户端:
#include <cstdio>
#include <unistd.h>
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
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);
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 <cstdlib>
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]);
UdpSocket sock;
sock.Socket();
while(1)
{
string buf;
cout << "请输入要发送的内容";
cin >> buf;
sock.Send(buf, srv_ip, srv_port);
buf.clear();
sock.Recv(&buf);
cout << buf << endl;
}
sock.Close();
return 0;
}
三、认识TCP协议
TCP(Transmission Control Protocol 传输控制协议),是面向连接的,可靠的字节流传输协议(确保了数据安全有序的到达对端,并且建立连接后才可以进行通信),TCP协议为了保证可靠传输,因此使用了很多的机制来完成,因此传输性能相对于UDP协议来说较低。
特点
应用场景:安全需求大于性能需求,比如文件传输。
四、TCP通信程序的编写
1.了解通信流程
a.客户端
- 创建套接字:关联网卡与进程。
- 绑定地址信息:不推荐主动绑定。
- 向服务端发起连接请求:如果没有主动绑定地址则会自动选择合适的地址进行绑定。连接一旦建立成功,客户端的socket中也会具有完整的五元组信息。
- 收发数据。
- 关闭套接字。
b.服务端
- 创建套接字:创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
- 绑定地址信息:为套接字绑定地址信息:描述sip和sport到socket中,用于告诉操作系统当收到数据时,这个数据要发送给所描述的sip和sport的时候,要交给这个socket进行处理。
- 开始监听:使socket进入listen状态,开始处理客户端连接请求。服务端会为每个新的客户端的连接请求创建一个新的socket,在内部描述完整的五元组信息,这个新建的套接字只与固定的客户端进行通信。
- 获取新建套接字的操作句柄:往后与指定的客户端进行通信都是通过新建的套接字(被称为连接套接字)完成的,原本的套接字(被称为监听套接字)只用来新建连接请求。
- 收发数据。
- 关闭套接字。
2. 通信接口认识
-
服务端开始监听 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关闭释放资源。
3. 程序编写
封装TCPSocket类
#include <iostream>
#include <unistd.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#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;
sock.Socket();
sock.Connect(srv_ip, srv_port);
while(1)
{
string data;
cout<<"clint input:";
fflush(stdout);
cin>>data;
sock.Send(data);
data.clear();
sock.Recv(&data);
cout<<"server response"<<data<<endl;
}
sock.Close();
return 0;
}
服务端
#include "TCPSocket.hpp"
#include <unordered_map>
unordered_map<string, string> table =
{
{"hello", "你好"},
{"goodmorning", "早上好"}
};
int main()
{
TCPSocket listen_sock;
listen_sock.Socket();
listen_sock.Bind("0.0.0.0", 9000);
listen_sock.Listen();
while(1)
{
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);
}
listen_sock.Close();
return 0;
}
|