文章目录
基础知识
- IP地址 :互联网协议地址(Internet Protocol Address),分配给互联网互联中设备的单一标识 ,理解成生活中的邮箱地址是比较类似的。在不同的地方入网,就会有不同的ip地址。
- 端口号(port) :没操作系统的知识的话,说的粗陋一点,就是如何识别在体态设备上与不同的APP通信,给这些APP分配一个ID,这个ID就叫做端口号。当然前面说法并不完全正确,准确的是端口号应该是区别一台主机中该网络信息向上交付时,是应该交由哪个进程处理 。而端口号就是用来做这个的 。和进程PID有点类似但又有所不同。
- 一个进程可以有多个port,因为其可以同时收到多个不同的网络消息。
- 一个port只能对应一个进程,因为他就是用来标识信息向上交付时交给哪个进程的
- 端口号的范围从0到65535,其中一些知名的端口(0-1023)被预留给标准服务,比如HTTP使用80端口,HTTPS使用443端口,FTP使用21端口等。而动态或私有端口(通常指从1024到65535)则通常是临时分配给应用程序的,尤其是在客户端或不固定服务场景中
- 网络字节序 :就是大小端存储的问题,网络中发送的数据需要考虑大小端,网络规定的标准是大端 。
- Big-endian:大端,指一个数据的低字节部分存在高地址处
- Little-endian:小端,。指一个数据他的低字节处 存储在低地址处。
- 例子:一个2字节数据:0x11 22 ,加入从左向右地址是增长的,那么大端里面:0x11 22 ;小端:0x22 11
- 这里说一下我自己容易搞错的点,数据的右边一般才是低字节序,数据的左边一般是高字节序列。你想一个整数,百位肯定是高位把。
- linux/windows系统都有提供的从主机转网络,网络转主机的函数
- htonl
- htons
- ntohl
- ntohs
socket编程
socket编程虽然里面需要填写的字段十分多,但是大多数场景下基本类似。
先来整体认识下一些必须要掌握的基础知识
常用接口(windows和linux的函数名都是一样的,但是由于系统不同,其中有些参数类型可能不太一样,以下以WIndows为例 ,linux你可以使用man + 函数名 查看具体情况)
这些常用接口除了类型有一些略微不同,其中许多宏常量,linux和Windows下几乎都是一样的。下面介绍的函数名windows和linux基本都共有。
- socket
cpp
SOCKET WSAAPI socket(
[in] int af, // 常常传入 AF_INET 或者AF_INET6
[in] int type,
[in] int protocol
);
-
- [in]是windows文档的一种习惯,表示这个参数是一个输入形参数
- af是协议家族, 常常传入 AF_INET 或者AF_INET6。你是ipv4地址就传入AF_INET,ipv6就选AF_INET6
- type 重点了解,UDP传入SOCK_DGRAM,TCP传入SOCK_STREAM。其余选项可自行取查看手册
- protocol:如果af,type指定了ipv4/6的协议家族和对应的类型,那么这里传入0由系统给你自动匹配协议是最好。的。如果你传入的是类似SOCK_RAW的值,那这里就需要你特殊指定,这个选项是用来精确控制套接字收到的数据。
-
返回值
如果未发生错误, 套接字 将返回引用新套接字的描述符。 否则,将返回值 INVALID_SOCKET,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
-
bind :一般服务端使用
cpp
* int bind(
[in] SOCKET s,
const sockaddr *addr,
[in] int namelen
);
-
参数
- [in] s
标识未绑定套接字的描述符。 - addr
指向要分配给绑定套接字 的本地地址 的 sockaddr 结构的指针。 - [in] namelen
addr 指向的值的长度(以字节为单位)。
- [in] s
-
返回值
-
如果未发生错误, 绑定 将返回零。 否则,它将返回SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
谈到bind,就必须提及Socket编程里面需要的用的结构体sockaddr ,sockaddr_in,sockaddr_in6
- 使用bind时,一般先创建都应的sockaddr_in(ipv4),sockaddr_in6(ipv6)的。填写其中的家族,端口号,和ip地址。
- 在强转成 sockaddr* 传入对应的函数bind处理即可。
- 其中要注意主机大小端和ip地址格式问题。用上述的网络字节序处的函数转变端口号。
- ip地址和和主机之间相互转换的时候,库里面的函数
如下:
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
in_addr_t inet_addr(const char *cp);
inet_ntop, inet_pton:这两个是都支持的下面几个用的相对较少,可以使用时查文档
ntohf
ntohl
ntohll
ntohs
cpp
struct sockaddr {
ushort sa_family;
char sa_data[14];
};
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
u_long sin6_scope_id;
};
上面两个操作TCP和UDP都会用到,但是下面介绍的就是TCP才用了
- listen :一般是TCP服务端代码使用
cpp
int WSAAPI listen(
[in] SOCKET s,
[in] int backlog
);
-
参数解释
- [in] s 标识绑定的未连接的套接字的描述符。
- [in] backlog 挂起的连接队列的最大长度。
-
返回值:如果未发生错误, 则侦听 返回零。 否则,将返回 值 SOCKET_ERROR ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
-
accept :一般时TCP服务器端使用
cpp
SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
-
参数
[in] s :一个描述符,用于标识已使用 侦 听函数置于侦听状态的套接字。 连接实际上是使用 accept 返回的套接字建立的。
[out] :addr指向接收连接实体地址的缓冲区的可选指针,该地址称为通信层。 addr 参数的确切格式由创建 sockaddr 结构中的套接字时建立的地址系列确定。
[in, out] :addrlen指向包含 addr 参数指向的结构长度的整数的可选指针。
-
返回值
果未发生错误, 则 accept 将返回 类型为 SOCKET 的值,该值是新套接字的描述符。 此返回值是建立实际连接的套接字的句柄。
否则,将返回 值 INVALID_SOCKET ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
这里返回值拿到的套接字正常读取写入就能和客户端通信了,当成文件描述符使用即可,当然用send和recv来收发数据也可以。
- connect :一般TCP客户端代码使用
-
参数
[in] s 标识未连接的套接字的描述符。
[in] name 指向应建立连接的 sockaddr 结构的指针。
[in] namelen name 参数指向的 sockaddr 结构的长度(以字节为单位)。
-
返回值
如果未发生错误, 则连接 返回零。 否则,它将返回SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
-
链接成功后就可以使用,当成文件描述符来使用也是没有问题的。当然用send和recv来收发数据也可以
UDP套接字编程
UDP套接字就只需要两个操作,socket和bind,做完之后UDP程序就能通信了。
具体步骤
服务器:
- 先申请一个套接字,填入对应的sockaddr的字段
- 再bind 改套接字即可
- 再用sendto 和 recv收发数据
客户端:
- 创建一个套接字
- 用sendto 和recv发收数据即可
Server
cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main() {
//使用windows网络库需要做的,linux下就没这过程
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "Failed to initialize winsock." << std::endl;
return -1;
}
SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serverSocket == INVALID_SOCKET) {
std::cerr << "Failed to create socket." << std::endl;
WSACleanup();
return -1;
}
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(12345); // 服务器监听端口
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cerr << "Failed to bind socket." << std::endl;
closesocket(serverSocket);
WSACleanup();
return -1;
}
std::cout << "Server is listening on port 12345..." << std::endl;
char buffer[1024];
sockaddr_in clientAddr;
int addrLen = sizeof(clientAddr);
while (true) {
int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (SOCKADDR*)&clientAddr, &addrLen);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0';
std::cout << "Message from client: " << buffer << std::endl;
// Echo back the message to the client
sendto(serverSocket, buffer, bytesReceived, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr));
}
}
closesocket(serverSocket);
WSACleanup();
return 0;
}
Client
cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "Failed to initialize winsock." << std::endl;
return -1;
}
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (clientSocket == INVALID_SOCKET) {
std::cerr << "Failed to create socket." << std::endl;
WSACleanup();
return -1;
}
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(12345); // 服务器端口号
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); // 服务器IP地址,这里假设为本地环回地址
std::string message;
std::cout << "Enter a message to send to the server: ";
std::getline(std::cin, message);
if (sendto(clientSocket, message.c_str(), message.length(), 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cerr << "Failed to send message." << std::endl;
} else {
std::cout << "Message sent to server." << std::endl;
}
char buffer[1024];
int bytesReceived = recvfrom(clientSocket, buffer, sizeof(buffer), 0, NULL, NULL);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0';
std::cout << "Reply from server: " << buffer << std::endl;
}
closesocket(clientSocket);
WSACleanup();
return 0;
}
TCP套接字编写
服务端:
- socket获取一个套接字
- bind这个套接字
- 监听这个套接字
- accept,接受链接成功返回一个套接字,用该套接字通信即可。用send和recv进行通信即可。可以接受多个套接字
客户端:
- socket 获取一个套接字
- connect 对应的服务器,成功后用该套接字 send和recv即可。
linux下一切皆文件,TCP这里读取内容的时候,其实用read和write来收发数据也是没有问题的。
服务器代码
cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed: %d\n", GetLastError());
return 1;
}
SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr("127.0.0.1"); // 或者使用INADDR_ANY监听所有地址
service.sin_port = htons(27015); // 自定义端口号
if (bind(ListenSocket, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
printf("listen failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Listening on port 27015...\n");
SOCKET ClientSocket;
while ((ClientSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET) {
char recvbuf[512];
int iResult = recv(ClientSocket, recvbuf, 512, 0);
if (iResult > 0)
printf("Received: %s\n", recvbuf);
else if (iResult == 0)
printf("Connection closing...\n");
else
printf("recv failed: %d\n", WSAGetLastError());
// 发送响应(可选)
char *response = "Hello from server!";
send(ClientSocket, response, strlen(response), 0);
closesocket(ClientSocket);
}
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
closesocket(ListenSocket);
WSACleanup();
return 0;
}
cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed: %d\n", GetLastError());
return 1;
}
SOCKET ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址
serverAddress.sin_port = htons(27015); // 与服务器端相同的端口号
if (connect(ConnectSocket, (SOCKADDR*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
char sendbuf[] = "Hello from client!";
int iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
char recvbuf[512];
iResult = recv(ConnectSocket, recvbuf, 512, 0);
if (iResult > 0)
printf("Received: %s\n", recvbuf);
else if (iResult == 0)
printf("Connection closed\n");
else
printf("recv failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 0;
}
套接字编写注意事项
- TCP和UDP都是全双工的,不用担心收发数据互相干扰的问题
- 为了收发不互相干扰,一般要采取并发