目录
.h头文件,用于配置链接(释放)网络库、创建服务器端SOCKET
.h头文件,用于配置链接(释放)网络库、创建客户端SOCKET
socket编程的作用:
socket编程即计算机网络编程,是为了让两台计算机上的应用进行远程连接、通信。
特点:
Socket是一个抽象层,主要是把TCP/IP层复杂的操作抽象为几个简单的接口提供给应用层调用,进而实现应用进程在网络中通信,只能访问到端到端协议之上的网络层和传输层。
实现:
以下仅示范由客户端向服务器端发送一次数据的实现方式。
网络库即socket库、套接字库。
由于socket编程可以让两台计算机进行通信,为了区别这两台计算机,称发送数据的计算机为客户端,接收数据的计算机称为服务器端 。
不管是在客户端还是服务器端都需要提前导入这两个库,第一个头文件<WinSock2.h>是windows下网络编程的规范,简单来说里面存储着SOCKET的数据结构和各种要用到的函数;第二个是用于静态加载ws2_32.lib库或者动态载入ws2_32.dll,提供了对网络相关API的支持。
#include<WinSock2.h> //windows平台的网络库头文件
#pragma comment(lib, "ws2_32.lib") //库文件
之后err函数就是这个,在出错时返回出错的行和错误码
#define err(errMsg) std::cout << "line: "<< __LINE__ << " " << errMsg << " failed code " << WSAGetLastError() << std::endl //__LINE__表示当前出错的行
服务器端:
简单分为三个步骤:链接网络库、创建socket服务器去获取信息、释放网络库。
链接网络库:
通过WSAStartup函是为了链接指定版本的socket库,链接成功返回0,非0则表示连接失败。这一步了调用各个版本的socket函数。
WSAStartup有两个参数,第一个参数是socket的版本号,用MAKEWORD(i, j)来表示,第二个参数是lpWSAData 类型的参数,是指向WSADATA数据结构的指针,用来接收Windows Sockets实现的细节。
注:WSADATA数据类型用来存储被WSAStartup函数调用后返回的 Windows Sockets数据。它包含Winsock.dll执行的数据等。
WSADATA wsadata;
//MAKEWORD(i, j)表示socket版本号为i.j。(不能直接输入i.j,也不能输入i, j)
WSAStartup(MAKEWORD(2, 2), &wsadata);
链接网络库部分代码:
bool init_Socket() //链接网络库
{
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { //返回0表示成功
std::cout << "WSAStartup failed code " << WSAGetLastError() << std::endl; //WSAGetLastError()用于获取最后一个错误码
return false;
}
return true;
}
创建socket服务器并获取信息:
这里包含两步:一、创建socket服务器。二、获取客户端发送的信息。
一、创建socket服务器:
注:在这一步里大写的SOCKET是数据类型,小写的socket是创建SOCKET的函数。
首先通过socket函数来创建一个SOCKET服务器,然后通过bind函数来绑定一个sockaddr类型的结构体(绑定要通信的应用),最后开启监听。
首先:socket函数有三个参数:第一个参数为地址协议族,常见的有ipv4(AF_INET)、ipv6(AF_INET6)、第二个参数为传输协议类型,常见的有流式套接字(SOCK_STREAM)、数据报(SOCK_DGRAM)、第三个参数表示某个具体的传输协议,常见的有TCP协议(IPPROTO_TCP)。如果创建SOCKET失败会返回INVALID_SOCKET。
//协议族为ipv4,使用流式套接字和TCP/IP协议。
SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd == INVALID_SOCKET) { //创建失败
err("socket");
return INVALID_SOCKET;
}
其次要给SOCKET服务器绑定IP地址和端口号,在绑定时使用的是sockaddr类型的结构体指针,但由于sockaddr类型结构体的IP地址和端口号混在一起,所以更经常使用sockaddr_in结构体指针强转为sockaddr类型结构体指针进行绑定。
sockaddr和sockaddr_in结构体的区别:
struct sockaddr
{
sa_family_t sin_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
先要创建一个sockaddr_in类型的结构体,输入协议族、IP地址和端口号(1024~65535、一般无法访问1024以下的端口号)。其中第七行被注释掉的inet_addr不安全,改用inet_pton函数绑定端口号。“127.0.0.1”代表本机的IP地址。
#include <WS2tcpip.h>
//创建一个sockaddr_in类型的结构体。
sockaddr_in addr;
addr.sin_family = AF_INET; //要和要绑定的socket的地址协议族相同。
//PORT是使用了宏替换的端口号,#define PORT 8888
addr.sin_port = htons(PORT); //绑定端口号,htons()函数将本地字节序转化为网络字节序
//addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //绑定ip地址,其中127.0.0.1是本地计算机ip,inet_addr(不能用了,要改)是为了转换ip地址的格式
// addr.sin_addr.S_un.S_addr = ADDR_ANY; //ADDR_ANY,绑定本机任意网卡的ip
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.S_un);
通过bind函数绑定socket服务器的IP地址和端口号,第一个参数是要绑定的SOCKET服务器,第二个参数是sockaddr类型的结构体指针,第三个参数是结构体的大小。如果返回SOCKET_ERROR,则绑定失败。
if (bind(fd, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { //如果绑定失败
err("bind");
return false;
}
最后是监听:监听使用listen函数,第一个参数是要接受信息的SOCKET服务器,第二个参数是最大支持的连接信息数。
listen(fd, 10);
到这一步创建SOCKET服务器就完成了,代码如下:
SOCKET CreatServerSocket()
{
SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd == INVALID_SOCKET) { //创建失败
err("socket");
return INVALID_SOCKET;
}
//给socket绑定ip地址和端口号
sockaddr_in addr;
addr.sin_family = AF_INET; //要和要绑定的socket的地址协议族相同。
addr.sin_port = htons(PORT); //绑定端口号,htons()函数将本地字节序转化为网络字节序
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.S_un);
if (bind(fd, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { //如果绑定失败
err("bind");
return false;
}
//监听
listen(fd, 10);
return fd;
}
二:获取客户端发送的信息:
通过accept函数从处于监听状态的流式套接字服务器的客户连接请求队列中取出最前的请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符;如果失败就返回INVALID_SOCKET。
该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。
accept函数默认会阻塞直到接收到信息。
SOCKET clifd = accept(serfd, NULL, NULL);
if (clifd == INVALID_SOCKET) { //如果客户端链接失败
err("accept");
}
当使用accept连接成功后,可以使用recv来接收客户端发送的信息并高存到缓冲区中国。第一个参数是accept返回的套接字,第二个参数是指向缓冲区的指针,第三个参数是缓冲区的长度。 返回值<0时表示出错;=0时表示连接关闭;>0时返回接受到的数据大小。 但如果有两个数据同时使用recv接受时可能会导致数据重叠。
//先创建一个缓冲区用于保存消息
char buf[BUFSIZ] = { 0 }; //BUFSIZ是宏,代表512
//recv从指定的socket接收数据保存到缓冲区中
if (recv(clifd, buf, BUFSIZ, 0) > 0) { //不断从客户端接收到数据
std::cout << buf << std::endl;
}
通过recv函数接受数据后保存在缓冲区buf中,可以通过查看buf中的数据查看客户端发送的信息。
释放网络库:
释放网络库时直接调用WSACleanup()函数,返回0表示成功。
bool close_Socket() //关闭套接字
{
if (WSACleanup() != 0) { //返回0表示成功
std::cout << "WSACleanup failed code " << WSAGetLastError() << std::endl; //获取最后一个错误码
return false;
}
return true;
}
客户端:
客户端需要链接网络库、创建socket客户端来发送信息、关闭网络库。
链接网络库:
和服务器端一样,不写。
创建socket客户端来发送信息:
分为两步:一、创建SOCKET客户端,二、发送信息。
一、创建SOCKET客户端:
同创建SOCKET服务器端,都是使用socket函数创建,但是它不需要绑定,而是通过connect函数链接服务器的地址,connect有三个参数,第一个是SOCKET客户端、第二个参数是要存储连接的服务器信息的sockaddr结构体指针,第三个参数是结构体的长度。链接失败则返回INVALID_SOCKET。
#include <WS2tcpip.h>
//与服务器建立连接
sockaddr_in addr;
addr.sin_family = AF_INET; //要和要绑定的socket的地址协议族相同。
addr.sin_port = htons(PORT); //绑定端口号,htons()函数将本地字节序转化为网络字节序
//在头文件《》中
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.S_un); //绑定服务器的ip地址
if (connect(fd, (sockaddr*)&addr, sizeof(addr)) == INVALID_SOCKET) {
err("connect");
return false;
}
二、发送信息:
通过send函数将存储在缓冲区中的消息发送给服务器,若失败则返回INVALID_SOCKET。send函数第一个参数为客户端SOCKET,第二个参数为指向存放要发送数据的缓冲区的指针,第三个参数为缓冲区的长度。
SOCKET fd = CreatClineSocket();
char buf[BUFSIZ] = "你好"; //BUFSIZ是512
if (send(fd, buf, strlen(buf), 0) == SOCKET_ERROR) {
err("send");
}
关闭网络库:
和服务器端一样,不写。
总代码如下:
先运行服务器端监听链接后运行客户端就可以在服务器端接收到消息。
服务器端:
.h头文件,用于配置链接(释放)网络库、创建服务器端SOCKET
#include <WS2tcpip.h>
bool init_Socket()
{
//WSA(Windows Socket Async windows异步套接字)
//MAKEWORD(i, j)表示socket版本号为i.j。(不能直接输入i.j)
//WSAStartup(MAKEWORD(2, 2), &wsadata); //参数一(parm1为请求的socket版本,参数二parm2为传出参数,是一个指针)
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { //返回0表示成功
std::cout << "WSAStartup failed code " << WSAGetLastError() << std::endl; //获取最后一个错误码
return false;
}
return true;
}
bool close_Socket() //关闭套接字
{
if (WSACleanup() != 0) { //返回0表示成功
std::cout << "WSACleanup failed code " << WSAGetLastError() << std::endl; //获取最后一个错误码
return false;
}
return true;
}
SOCKET CreatServerSocket()
{
//创建空的socket
/*第一个参数为地址协议族,常见的有ipv4(AF_INET)、ipv6(AF_INET6).。
* 第二个参数为传输协议类型,创建的有流式套接字(SOCK_STREAM)、数据报(SOCK_DGRAM)。
* 第三个参数表示某个具体的传输协议,常见的有TCP协议(IPPROTO_TCP)。
*/
SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd == INVALID_SOCKET) { //创建失败
err("socket");
return INVALID_SOCKET;
}
//给socket绑定ip地址和端口号
sockaddr_in addr;
addr.sin_family = AF_INET; //要和要绑定的socket的地址协议族相同。
addr.sin_port = htons(PORT); //绑定端口号,htons()函数将本地字节序转化为网络字节序
//addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //绑定ip地址,其中127.0.0.1是本地计算机ip,inet_addr(不能用了,要改)是为了转换ip地址的格式
// addr.sin_addr.S_un.S_addr = ADDR_ANY; //ADDR_ANY,绑定本机任意网卡的ip
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.S_un);
/*bind函数 第一个参数为要绑定的socket。
* 第二个参数是一个sockaddr结构体的指针,里面存储要绑定的ip地址和端口号。
* 第三个参数是第二个参数指向的结构体的长度。
*/
if (bind(fd, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { //如果绑定失败
err("bind");
return false;
}
//监听
/*第一个参数为要监听的socket
*/
listen(fd, 10);
return fd;
}
.cpp文件,运行
#include "tcpSocket.h"
int main() {
init_Socket(); //打开网络库
//创建一个socket服务器
SOCKET serfd = CreatServerSocket();
std::cout << "等待客户端连接" << std::endl;
//如果有客户端请求链接
SOCKET clifd = accept(serfd, NULL, NULL);
if (clifd == INVALID_SOCKET) { //如果客户端链接失败
err("accept");
}
else { //否则,服务器端可以和客户端通信
//先创建一个缓冲区用于保存消息
char buf[BUFSIZ] = { 0 }; //BUFSIZ是宏,代表512
/* 第一个参数是要指定的socket
* 第二个参数是指向缓冲区的指针
* 第三个参数是缓冲区的长度
*/
//recv从指定的socket接收数据保存到缓冲区中
if (recv(clifd, buf, BUFSIZ, 0) > 0) { //不断从客户端接收到数据
std::cout << buf << std::endl;
}
}
closesocket(clifd); //先关闭客户端再关闭服务器
closesocket(serfd);
close_Socket(); //关闭网络库
system("pause");
return 0;
}
客户端:
.h头文件,用于配置链接(释放)网络库、创建客户端SOCKET
#include <WS2tcpip.h>
bool init_Socket()
{
//WSA(Windows Socket Async windows异步套接字)
//MAKEWORD(i, j)表示socket版本号为i.j。(不能直接输入i.j)
//WSAStartup(MAKEWORD(2, 2), &wsadata); //参数一(parm1为请求的socket版本,参数二parm2为传出参数,是一个指针)
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { //返回0表示成功
std::cout << "WSAStartup failed code " << WSAGetLastError() << std::endl; //获取最后一个错误码
return false;
}
return true;
}
bool close_Socket() //关闭套接字
{
if (WSACleanup() != 0) { //返回0表示成功
std::cout << "WSACleanup failed code " << WSAGetLastError() << std::endl; //获取最后一个错误码
return false;
}
return true;
}
SOCKET CreatClineSocket()
{
//创建空的socket
/*第一个参数为地址协议族,常见的有ipv4(AF_INET)、ipv6(AF_INET6).。
* 第二个参数为传输协议类型,创建的有流式套接字(SOCK_STREAM)、数据报(SOCK_DGRAM)。
* 第三个参数表示某个具体的传输协议,常见的有TCP协议(IPPROTO_TCP)。
*/
SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd == INVALID_SOCKET) { //创建失败
err("socket");
return INVALID_SOCKET;
}
//与服务器建立连接
sockaddr_in addr;
addr.sin_family = AF_INET; //要和要绑定的socket的地址协议族相同。
addr.sin_port = htons(PORT); //绑定端口号,htons()函数将本地字节序转化为网络字节序
//在头文件《WS2tcpip.h》中
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.S_un); //绑定ip地址,其中127.0.0.1是本地计算机ip,inet_addr(不能用了,要改)是为了转换ip地址的格式
if (connect(fd, (sockaddr*)&addr, sizeof(addr)) == INVALID_SOCKET) {
err("connect");
return false;
}
return fd;
}
.cpp文件,运行
#include"tcpSocket.h"
int main() {
init_Socket();
SOCKET fd = CreatClineSocket();
char buf[BUFSIZ] = "你好";
//发送消息给服务器
if (send(fd, buf, strlen(buf), 0) == SOCKET_ERROR) {
err("send");
}
closesocket(fd);
system("pause");
close_Socket();
return 0;
}