目录
一.简单了解TCP和UDP协议
二.网络字节序
三.socket常见的编程接口
1.介绍接口
2.sockaddr结构
四.简单的UDP网络程序
1.recvfrom和sendto
2.server.cc
3.client.cc
五.简单的TCP通信
1.client.cc
2.server.cc
一.简单了解TCP和UDP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论。
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
二.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
// htonl() 将无符号整数,从主机字节顺序转换为网络字节顺序。
uint32_t htonl(uint32_t hostlong);
// htons() 将无符号短整数,从主机字节顺序转换为网络字节顺序。
uint16_t htons(uint16_t hostshort);
// ntohl() 将无符号整数,从网络字节顺序转换为主机字节顺序。
uint32_t ntohl(uint32_t netlong);
// ntohs() 将无符号短整数,从网络字节顺序转换为主机字节顺序。
uint16_t ntohs(uint16_t netshort);
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
三.socket常见的编程接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr,socklen_t addrlen);
1.介绍接口
socket
- 功能:创建套接字,表明我们需要的通信服务类型,和IP协议,tcp/udp通信都需要使用。
- 参数:_1:通信类型,_2:协议类型,_3:默认给0。
- 返回值:成功返回一个文件描述符,失败返回-1,错误码被设置。
bind
- 功能:将端口号与该进程绑定,tcp/udp通信都需要使用。
- 参数:_1:socket返回的文件描述符,_2:一种特定的结构,存储IP地址和端口号,通信协议类型。_3:这种特定结构的大小。
- 返回值:成功返回0,失败返回-1,错误码被设置。
listen
- 功能:为套接字设置backlog,并创建半连接队列和全连接队列,将TCP状态设置为TCP_LISTEN,tcp服务端通信才能使用。
- 参数:_1:socket返回的文件描述符,_2:输出型参数,存储客户端的IP和端口信息,_3:输出型参数,第一个参数存储客户端信息结构的大小。
- 返回值:成功返回0,失败返回-1,错误码被设置。
accept
- 功能:接收来自客户端的连接请求,tcp服务端通信才能使用。
- 参数:_1:socket返回的文件描述符,_2:接受客户端的最大数量。
- 返回值:成功返回一个新的套接字专门用于和客户端进行通信,失败返回-1。
connect
- 功能:函数会发起一个TCP连接请求。tcp客户端使用。
- 参数:_1:socket返回的文件描述符,_2:一种特定的结构,存储IP地址和端口号,通信协议类型。_3:这种特定结构的大小。
- 返回值:成功返回0,失败返回-1,错误码被设置。
2.sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,
- 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
struct sockaddr_in结构 :
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; //端口
struct in_addr sin_addr; //ip地址
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
四.简单的UDP网络程序
1.recvfrom和sendto
recvfrom
- 功能:从指定的套接字上接收数据。
- 参数:_1:文件描述符,_2:存储缓冲区,_3:标志位,一般为0,_4:数据发送者的地址和端口,_5:地址结构的大小。
- 返回值:实际接收到的字节数,如果发生错误则返回-1。如果套接字被关闭或连接已断开,则返回0。
sendto
- 功能:向指定的目标地址发送数据报.
- 参数:_1:创建好的socket的文件描述符,_2:指向需要发送数据的缓冲区的指针,_3要发送的数据的长度,_4:通常设为0,_5:这是一个指向包含数据报接收者的协议地址(如IP地址和端口号)的套接字地址结构的指针,其大小由addrlen参数指定,_6:这是一个指向sockaddr结构大小的指针。
- 返回值:如果数据成功发送,则返回实际发送的字节数;如果出现错误,则返回-1。
2.server.cc
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
const static int port = 8080;
class Udp_server
{
enum
{
scket_err = 1,
bind_err
};
public:
Udp_server(int po = port)
: _port(po)
{
}
void Init()
{
// 一.创建套接字,返回文件描述符,
// 1.AF_INET:表示支持IPv4网络通信
// 2.SOCK_DGRAM:支持无连接的不可靠的面向数据报的传输方式,即udp传输
// 3.0:只支持一个协议来给定套接字
_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_fd == -1)
{
cout << "create socket error:" << strerror(errno) << endl;
exit(scket_err);
}
else
{
cout << "create socket successful" << endl;
}
struct sockaddr_in local;
// 清空
bzero(&local, sizeof(local));
// 写入端口号,htons:主机序列转网络序列
local.sin_port = htons(_port);
// 写入AF_INET,表示网络通信。
local.sin_family = AF_INET;
// 写入IP地址,inet_addr:
// 1.将点分十进制ip,转化为整形,
// 2.并且将主机序列转换为网络序列
// 3.最为一款云服务器,或者服务器,一般不止一个网卡,或者IP,在设置IP时不能直接将ip设置死
local.sin_addr.s_addr = INADDR_ANY;
// 二.绑定
if (bind(_fd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error:" << strerror(errno) << endl;
exit(bind_err);
}
else
{
cout << "bind successful" << endl;
}
}
void Start()
{
// 1.接受信息
char buff[1024];
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
while (1)
{
int read_size = recvfrom(_fd, buff, sizeof(buff), 0, (struct sockaddr *)&client, &client_len);
if (read_size > 0)
{
buff[read_size] = 0;
}
cout << "client:" << buff << endl;
// 提取客户端信息
string clientip = inet_ntoa(client.sin_addr); // ip
int clientport = ntohs(client.sin_port);
// 发送信息
char send[1024];
sprintf(send, "(-_-) %s (-_-)", buff);
sendto(_fd, send, sizeof(send), 0, (const struct sockaddr *)&client, client_len);
}
}
~Udp_server()
{
}
private:
int _port;
int _fd;
};
int main()
{
cout << "00000000" << endl;
Udp_server server(8081);
server.Init();
server.Start();
return 0;
}
3.client.cc
#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
//./client ip port
int main(int argc, char *args[])
{
// 1.创建按套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
// 2.获取ip和端口
string server_ip = args[1];
int server_prot = atoi(args[2]);
// 3.明确连接的服务器
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_prot);
while (1)
{
string str;
str.clear();
getline(cin, str);
// 发送信息
sendto(fd, str.c_str(), str.length(), 0, (struct sockaddr *)&server, sizeof(server));
// 接受信息
char buff[1024];
struct sockaddr_in sersock;
socklen_t sersock_len = sizeof(sersock);
//
int n = recvfrom(fd, buff, sizeof(buff), 0, (struct sockaddr *)&sersock, &sersock_len);
if (n > 0)
buff[n] = 0;
else
continue;
cout << "server:" << buff << endl;
}
return 0;
}
代码测试:
五.简单的TCP通信
1.client.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;
enum err
{
SOCKER_ERR = 1,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR,
CONNECT_ERR
};
int main(int argc, char *argv[])
{
string port = argv[2];
string ip = argv[1];
// 创建TCP套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 向服务端建立连接
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(port.c_str()));
// net_aton()将网络主机地址cp从IPv4数字和点符号转换为二进制形式(以网络字节为单位)
// 并将其存储在inp指向的结构中
inet_aton(ip.c_str(), &server.sin_addr);
int count = 1;
while (connect(sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
sleep(1);
cout << "正在尝试重连..."
<< "[" << count++ << "/5]" << endl;
if (count == 6)
exit(CONNECT_ERR);
}
// 连接成功
while (1)
{
string message;
cout << "Enter>>> ";
getline(cin, message);
write(sock, message.c_str(), message.length());
char buff[1024];
int n = read(sock, buff, sizeof(buff));
if (n > 0) // 读取成功
{
buff[n] = 0;
cout << "server:" << buff << endl;
}
else if (n == 0) // 服务器端关闭了
{
cout << "server quit,me too" << endl;
break;
}
else // 读取出错
{
cout << "read err" << endl;
break;
}
}
close(sock);
return 0;
}
2.server.cc
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
const int static backlog = 32;
enum err
{
SOCKER_ERR = 1,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR,
CONNECT_ERR
};
class tcp_server;
struct Args
{
Args(int sock, uint16_t port, string ip, tcp_server *server)
: _sock(sock), _port(port), _ip(ip), _server(server)
{
}
int _sock;
uint16_t _port;
string _ip;
tcp_server *_server;
};
class tcp_server
{
public:
tcp_server(uint16_t port, function<string(string)> func)
: _port(port), _func(func)
{
}
void Init()
{
// 1.创建套接字
// AF_INET:使用IPv4通信。
// SOCK_STREAM:面向字节流,可靠的,有序,双向的,tcp通信。
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
cerr << "socket err:" << strerror(errno) << endl;
exit(SOCKER_ERR);
}
else
{
cerr << "socket successful" << endl;
}
// 2.绑定
struct sockaddr_in host;
memset(&host, 0, sizeof(host));
host.sin_family = AF_INET; // #define AF_INET 2
host.sin_port = htons(_port);
host.sin_addr.s_addr = htonl(INADDR_ANY); // #define INADDR_ANY 0
if (bind(_listensock, (struct sockaddr *)&host, sizeof(host)) == -1)
{
cerr << "bind err:" << strerror(errno) << endl;
exit(BIND_ERR);
}
else
{
cerr << "bind successful" << endl;
}
// 3.监听
if (listen(_listensock, backlog) == -1)
{
cerr << "listen err:" << strerror(errno) << endl;
exit(LISTEN_ERR);
}
else
{
cerr << "listen successful" << endl;
}
}
void Start()
{
// v2.1子进程回收
signal(SIGCHLD, SIG_IGN);
while (1)
{
// 4.接受请求
// 表明接受的客户端主机
struct sockaddr_in client_host;
socklen_t client_len = sizeof(client_host);
int sock = accept(_listensock, (struct sockaddr *)&client_host, &client_len);
if (sock == -1)
{
cerr << "accpet err:" << strerror(errno) << endl;
exit(ACCEPT_ERR);
}
else
{
cerr << "socket successful,get a new client"
<< "fd:" << sock << endl;
}
// 提取客户端信息
uint16_t client_port = ntohs(client_host.sin_port);
// inet_ntoa: 将网络主机地址(以网络字节序)转换为IPv4点分十进制的字符串.
string client_ip = inet_ntoa(client_host.sin_addr);
// 多线程处理接受的客户端
pthread_t t;
pthread_create(&t, nullptr, threadRun, new Args(sock, client_port, client_ip, this));
}
}
static void *threadRun(void *args)
{
pthread_detach(pthread_self());
Args *ts = (Args *)(args);
ts->_server->service(ts->_sock, ts->_port, ts->_ip);
return nullptr;
}
void service(int sock, uint16_t client_port, string client_ip)
{
string name = to_string(client_port) + "-" + client_ip;
char buff[1024];
while (1)
{
int n = read(sock, buff, sizeof(buff) - 1);
if (n > 0) // 读取成功
{
buff[n] = 0;
// 读取成功之后,处理也业务
string recvmess = _func(name + ":" + buff);
// 将信息返回
write(sock, recvmess.c_str(), recvmess.size());
}
else if (n == 0)
{
// 对方将连接关闭了
close(sock);
std::cout << name << " quit, me too" << std::endl;
break;
}
else
{
close(sock);
cout << "read err" << strerror(errno) << endl;
break;
}
}
}
private:
uint16_t _port; // 端口
int _listensock; // 监听套接字
function<string(string)> _func; // 服务端处理的业务
};
string func(string message)
{
return message + "(-_-)";
}
int main(int argc, char *argv[])
{
int port = atoi(argv[1]);
tcp_server server(port, func);
server.Init();
server.Start();
return 0;
}
代码测试: