文章目录
- 服务器编程
- 1.创建服务端套接字
- 2.绑定服务端套接字
- 3.服务端启动
- 客户端编程
- 1.创建客户端套接字
- 2.绑定客户端套接字
- 服务器和客户端测试
服务器编程
1.创建服务端套接字
使用socket函数调用可以创建套接字的文件描述符,与前边的文件类似,socket函数的返回值是文件描述符,其实套接字是被进程创建的,例如在服务器端的某一个进程需要与客户端的一个进程进行网络通信就要创建套接字,进程的管理是通过进程控制块PCB来实现的,创建套接字就是创建了一个struct_files结构体,而在PCB:task_struct
中有一个struct files_struct* files的指针,指针指向一个表,这个表中有一部分是进程的文件描述符表:fd_array
,在该表的某一位置写入该结构体的指针,就是创建了套接字。
当我们创建一个套接字时,其实就是进程打开了一个网络文件,而打开文件就会创建文件结构体,并将地址填入PCB的文件描述符表中,所以socket函数的返回值就是文件描述符,只打开一个文件,文件描述符就是3,前边分别打开了标准输入,标准输出,标准错误,并且这些文件结构体是通过双向链表的结构保存的。
每一个文件结构体struct_file中都包含该文件的属性,该文件的缓冲区,该文件的操作方法,例如如何打开和关闭文件,因为在系统中,我们可以把一切硬件看作文件,只是给予不同的操作方法就可以操作文件,而对于普通文件来说,缓冲区被刷新后对应的地方就是硬盘,而对于网络编程套接字来说,缓冲区刷新后,把数据发送到网卡。
socket函数
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:
domain
:套接字的域,也就是套接字的类型,例如常用的两个域:
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
type
:套接字创建时的服务类型,例如:
SOCK_STREAM 表示流协议
SOCK_DGRAM 表示数据报协议
protocol
:通常传入0,可以根据前边的参数,推出使用UDP协议还是TCP协议返回值:返回值就是创建套接字成功后返回的文件描述符,错误返回-1.
2.绑定服务端套接字
在创建好套接字之后,本质上就是在系统层面打开了一个文件结构体,但是此时系统并不清楚打开的文件是网络文件还是普通文件,所以就要将套接字与IP和端口号进行绑定,在绑定套接字时,必须先传入一个sockaddr_in类型的结构体,并且必须使用IP,端口号,协议家族进行填充,此时就可以将套接字与网络进行绑定。
bind函数
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
参数:
socket
:前边创建套接字时生成的对应文件描述符
address
:sockaddr_in或者sockaddr_un的地址,选择对应网络通信还是域间通信
len
:传入sockaddr结构体的长度返回值
绑定成功返回0,失败返回-1
但是还有一些要注意的地方:
- bind函数的第二个参数是struct sockaddr* ,所以在传参时,必须进行强转。
- 在对sockaddr_in结构体进行填充时,端口号从主机发送到网络,必须使用htons或者htonl函数将端口号转为大端模式。
- 在填充时,IP地址为了让用户观察,所以设置为点分十进制,但是发送到网络之后,确是一个四字节的整数,所以必须要进行转换,使用inet_addr接口可以将点分十进制转换为四字节整数,而使用inet_ntoa可以将四字节整数转换为点分十进制,并且这两个接口也有前边处理大小端问题的作用。
sockaddr_in的结构
在sockaddr_in结构体中主要有IP地址,端口号,已经协议家族,在绑定时,套接字就是根据sockaddr_in 中填充的这些数据来绑定。
server.hpp
class udp_server
{
public:
udp_server(const uint16_t& port, const std::string& ip = "0.0.0.0")
: _sockfd(-1), _ip(ip), _port(port)
{}
void server_init()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
std::cout << "socket success: " << _sockfd << std::endl;
// 将套接字与网络绑定
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_family = AF_INET;
local.sin_port = htons(_port);
if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
}
private:
int _sockfd;
std::string _ip;
uint16_t _port;
};
server.cc
#include<iostream>
#include"udp_server.hpp"
#include<memory>
#include <cstdlib>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<udp_server> svr(new udp_server(port));
svr->server_init();
svr->start();
return 0;
}
3.服务端启动
由于服务器必须不间断的提供服务,所以必须不断循环等待客户端发送消息,首先要对sockaddr_in结构体进行填充,通过recvfrom接口接收数据时,同时也可以接收到客户端的IP地址和端口号,因为处理完毕要发送的时候就可以知道要给哪一个IP的哪个进程。
recvfrom接口
参数
sockfd:创建套接字生成的文件描述符
buf:要接收内容的缓冲区
len:接收内容的字节数
flags:读取方式,一般设置为0,阻塞读取
src_addr:对端网络的相关属性,例如协议家族,IP地址,端口号
addrlen:期望读取对端网络结构体的长度,这是一个输入输出型参数返回值
读取成功返回0,失败返回-1
sendto接口
参数:
sockfd:套接字的文件描述符
buf:要发送的数据
len:期望发送数据的字节数
flags:写入的方式,通常为0,表示阻塞写入
dest_addr:目标网络的属性,包含通信协议,IP地址,端口地址
addrlen:传入sockaddr结构体的大小\返回值:
成功发送返回0,发送错误返回-1
void start()
{
char buffer[SIZE];
for (;;)
{
struct sockaddr_in peer;
socklen_t size = sizeof(peer);
bzero(&peer, sizeof(peer));
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &size);
if (s > 0)
{
buffer[s] = 0;
uint16_t client_port = ntohs(peer.sin_port);
std::string client_ip = inet_ntoa(peer.sin_addr);
printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, buffer);
}
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, size);
}
}
客户端编程
1.创建客户端套接字
客户端套接字的创建与服务端的创建类似,只是在创建套接字之后,不需要进行绑定端口号和IP地址,因为服务器是给为了给别人提供服务,所以必须让别人知道IP地址和端口号,而且不能轻易更改,但是客户端的端口号只要是唯一的就可以了,不需要进行绑定,如何客户端只是绑定唯一的一个端口号,那么当一个进程没有启动,那么这个端口也不能给别人使用,如果这个歌这个端口被别人使用了,此时这个进程也就不能启动了,所以不进行绑定,而是在进程启动时分配端口号,可以提高利用率。
client.cc
#include<iostream>
#include"udp_client.hpp"
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
udp_client* clt = new udp_client(server_port,server_ip);
clt->client_init();
clt->start();
return 0;
}
client.hpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include "log.hpp"
#define SIZE 1024
class udp_client
{
public:
udp_client(uint16_t port, std::string ip)
: _sockfd(-1), _port(port), _ip(ip)
{
}
bool client_init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
return true;
}
~udp_client()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
};
2.绑定客户端套接字
首先要对sockaddr_in结构体进行填充,填充时要注意前边的几点,但是在将字符串风格的IP地址转换为整形风格的IP时,首先要将string类型转为C语言风格的字符串。
void start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_port);
peer.sin_addr.s_addr = inet_addr(_ip.c_str());
std::string msg;
for (;;)
{
char buffer[SIZE];
std::cout << "please input#";
std::getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
struct sockaddr_in temp;
socklen_t size = sizeof(temp);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&temp, &size);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
}
服务器和客户端测试
直接运行,说明书提示,服务器必须提供端口号,客户端必须提供服务器的端口号和IP
当我们正确的写上命令行参数时,就可以进行通信。我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为8081,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8081。
同时,我们可以使用netstat指令来获得网络信息。
在进行本地测试之后,也可以将自己的公网IP和端口号告诉朋友,可以让朋友试一下是否可以进行网络通信: