兄弟萌用虚拟机装上新系统或者租用云服务器之后,总想要能在PC和服务器之间传些东西。
一般说来,Filezilla等FTP工具就很好用了(甚至支持拖拽式上传下载)。但是,能写写代码当然更有意思。本文介绍Windows环境下的实现方式(Linux环境代码上的区别很小,文中会指出),提供全部的代码。
socket这玩意儿,就是服务端监听某个端口,根据客户端的请求返回对应的内容。这样说来,服务端需要解析客户端的请求,那就涉及到请求格式和格式解析,很麻烦嘛。不过如果我们固定返回某句话,或者某个文件,这样就简单了。而且有了完整的应答接口,后续再加上解析也是可以的。
经过简化,这个看似工程量很大的计划,就转化成实现下面三个函数啦。
1 SOCKET Socket_SetupSever(const std::string& address, const unsigned short& port);
2 std::string Socket_ClientGet(const std::string& address, const unsigned short& port);
3 std::string Socket_Socket_ReadInfo(const std::string& infopath, char mod);
三个函数按顺序依次是1
在服务器建立服务、2
在客户端向服务端发起请求、3
获取准备向客户端返回的文件内容。
那服务端就可以用下面的代码建立服务
/* 建立端口监听服务 */
SOCKET servSock = Socket_SetupSever("127.0.0.1",1234);
/* 接收客户端请求 */
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
/* 准备通过clntSock向客户回传数据 */
while (true) {
SOCKET clntSock = accept(servSock, (SOCKADDR*)& clntAddr, &nSize);
if (clntSock != 0) {
/* 向客户端发送数据 */
/* 这里设计成两个接口——简单字串和文件,类型由第二个参数确定 */
std::string info = Socket_Socket_ReadInfo("info.txt",'f');
send(clntSock, info.c_str(), info.size(), NULL);
//关闭此位client的socket
closesocket(clntSock);
}
}
closesocket(servSock);
首先用Socket_SetupSever
获取配置好的socket
,这里在本地做实验,IP配置为127.0.0.1
,端口号为1234
,你也可以选其他可用的端口。
然后进入While
循环中,用accpe()
t持续监听是否有client
访问本地的1234端口。
如果有客户成功连接(if (clntSock != 0)
),那就用Socket_Socket_ReadInfo()
获取准备返回的内容,用send()
发回客户端。
客户端只需要用Socket_ClientGet()
发起请求就行啦,这里把返回的内容输出到标准输出。
std::cout << Socket_ClientGet("127.0.0.1", 1234) << endl;
话不多说,上菜。
第一个是建立端口监听服务的Socket_SetupSever
,以主机IP和开放端口为参数,返回配置好的Socket。
/*
Socket_SetupSever:建立端口监听服务
param:
address:服务器主机ip
port:服务器准备监听的端口
retval:根据参数配置好的socket
*/
SOCKET Socket_SetupSever(const std::string& address, const unsigned short& port)
{
//创建套接字
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr(address.c_str()); //具体的IP地址
sockAddr.sin_port = htons(port); //端口
bind(servSock, (SOCKADDR*)& sockAddr, sizeof(SOCKADDR));
//进入监听状态
listen(servSock, 20);
return servSock;
}
第二个是在客户端发起请求的Socket_ClientGet()
/*
Socket_ClientGet:发起客户请求
param:
address:目标服务器ip
port:服务器提供回传服务的端口
retval:服务器返回的信息
*/
std::string Socket_ClientGet(const std::string& address, const unsigned short& port) {
/* 获取sccket */
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
/* 配置请求信息 */
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr(address.c_str());
sockAddr.sin_port = htons(port);
/* 发起请求 */
connect(sock, (SOCKADDR*)& sockAddr, sizeof(SOCKADDR));
/* 接收服务器回传数据 */
char szBuffer[MAXINT16] = { 0 };
recv(sock, szBuffer, MAXINT16, NULL);
/* 关闭socket */
closesocket(sock);
return std::string(szBuffer);
}
第三个是获取返回信息的Socket_Socket_ReadInfo()
/*
Socket_Socket_ReadInfo:获取向clinet回传的数据
param:
infopath:
mod: 说明info的来源,默认为's',指明为'f'时,是文件路径
retval:mod=='s'时返回infopath,mod=='f'时返回文件内容
*/
std::string Socket_Socket_ReadInfo(const std::string& infopath,char mod='s')
{
if (mod == 's') {
return infopath;
}else {
std::ifstream f(infopath);
std::string text, te;
while (getline(f, te)) {
text.append(te);
}
return text;
}
}
注意第一部分展示的服务端代码中有一句
std::string info = Socket_Socket_ReadInfo("info.txt",'f');
就是返回文本文件info.txt
的内容。而如果写成
std::string info = Socket_Socket_ReadInfo("info.txt");
返回的就是值为"info.txt"
的string类型对象。
把上面介绍的三个函数放到头文件"socketpocket.h"
中。
#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
SOCKET Socket_SetupSever(const std::string& address, const unsigned short& port);
std::string Socket_Socket_ReadInfo(const std::string& infopath, char mod);
std::string Socket_ClientGet(const std::string& address, const unsigned short& port);
#include
#include "socketpocket.h"
using namespace std;
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
/* 建立端口监听服务 */
SOCKET servSock = Socket_SetupSever("127.0.0.1",1234);
/* 接收客户端请求 */
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
/* 准备通过clntSock向客户回传数据 */
while (true) {
SOCKET clntSock = accept(servSock, (SOCKADDR*)& clntAddr, &nSize);
if (clntSock != 0) {
/* 向客户端发送数据 */
/* 这里设计成两个接口——简单字串和文件,类型由第二个参数确定 */
std::string info = Socket_Socket_ReadInfo("info.txt",'f');
send(clntSock, info.c_str(), info.size(), NULL);
//关闭此位client的socket
closesocket(clntSock);
}
}
closesocket(servSock);
//终止 DLL 的使用
WSACleanup();
return 0;
}
#include
#include "socketpocket.h"
int main() {
/* 动态链接库相关 */
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
/* 发起连接,将返回信息输出到标准输出 */
std::cout << Socket_HttpGet("127.0.0.1", 1234) << endl;
/* 停用动态链接库 */
WSACleanup();
system("pause");
return 0;
}
Windows 下的 socket 程序和 Linux 思路相同,但细节有所差别:
Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。DLL 有两种加载方式,请查看:动态链接库DLL的加载
Linux 使用“文件描述符”的概念,而 Windows 使用“文件句柄”的概念;Linux 不区分 socket 文件和普通文件,而 Windows 区分;Linux 下 socket()
函数的返回值为 int 类型,而 Windows 下为 SOCKET 类型,也就是句柄。
Linux 下使用 read()
/ write()
函数读写,而 Windows 下使用 recv()
/ send()
函数发送和接收。
关闭 socket 时,Linux 使用 close()
函数,而 Windows 使用 closesocket()
函数。
测试示例代码先运行服务器端,再运行客户端,文件路径用绝对路径,要保证对应的文件存在并且可读。