目录
[一.端口号 VS PID](#一.端口号 VS PID)
一.端口号 VS PID
pid已经能够标识一台主机上的一个唯一一个进程了,为什么还需要端口号?
- 不是所有的进程都需要网络通信,但是所有的进程都需要都pid;
- 系统和网络功能解耦。
另外,一个进程可以绑定多个端口,但一个端口只能被一个进程绑定。
系统内定的端口号【0,1023】一般都要有固定的应用层协议使用,如http:80,https:443。
二.套接字编程的类型
- 域间套接字编程------同一个机器内
- 原始套接字编程------网络工具
- 网络套接字编程------用户间的网络通信
不同的套接字编程类型的接口需要是相同的。

如同用一个统一的sockaddr作为基类,sockaddr_in和sockaddr_un作为子类。使用的时候我们用if语句区分具体是什么类型,如同下面这段代码:
cpp
if(address->type == AF_INET)
{
// 网络套接字
}
else if(address->type == AF_UNIX)
{
// 域间套接字
}
为什么操作系统接口设计者在设计的时候不直接使用void*呢,因为那时候C语言还没有void*。
三.socket编程接口
cpp
// 创建 socket 文件描述符(TCP/UDP,客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 开始监听socket(TCP,服务器)
int listen(int sockfd, int backlog);
// 接收请求(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
四.基于udp的服务端和客户端全部代码
客户端
代码如下:
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
// #include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h> // sockaddr_in
using namespace std;
void usage(const std::string& proc)
{
cout << "Please use like this : " + proc + " [ip] [port]" << endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockFd < 0) cerr << "sockFd error" << endl;
struct sockaddr_in server;
socklen_t sz = sizeof(server);
bzero(&server, sz);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
std::string temp;
while(true)
{
cout << "请输入消息@ ";
getline(cin, temp);
int ret = sendto(sockFd, temp.c_str(), temp.size(), 0, (struct sockaddr*)&server, sz);
if(ret < 0) std::cerr << "send error" << std::endl;
char buffer[1024];
struct sockaddr_in retServer;
socklen_t len = sizeof(retServer);
int n = recvfrom(sockFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&retServer, &len);
if(n > 0)
{
buffer[n] = 0;
cout << "收到消息:" << buffer << endl;
}
}
close(sockFd);
return 0;
}
服务端
代码如下:
UdpServer.hpp:
cpp
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <unistd.h>
// #include <netinet/in.h>
#include <arpa/inet.h> // sockaddr_in
#include <iostream>
#include <vector>
#include <functional>
using func_t = std::function<std::string(const std::string&)>;
const std::string defaultip = "0.0.0.0";
const uint16_t defaultport = 8080;
class UdpServer
{
public:
UdpServer():_sockFd(-1)
{}
~UdpServer()
{
if(_sockFd >= 0) close(_sockFd);
}
void init(const std::string& ip = defaultip, uint16_t port = defaultport)
{
_sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockFd < 0) std::cerr << "创建套接字失败" << std::endl;
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip.c_str());
local.sin_port = htons(port);
int ret = bind(_sockFd, (const struct sockaddr*)&local, sizeof(local));
if(ret < 0) std::cerr << "绑定本地套接字信息失败" << std::endl;
}
void run(func_t func)
{
while(true)
{
struct sockaddr_in client;
socklen_t sz = sizeof(client);
char buffer[1024];
int n = recvfrom(_sockFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&client, &sz);
buffer[n] = 0;
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
std::cout << "[" << clientip + ":" << clientport << "] send information : " << buffer << std::endl;
std::string temp = func(buffer);
int ret = sendto(_sockFd, temp.c_str(), temp.size(), 0, (const struct sockaddr*)&client, sz);
if(ret < 0) std::cerr << "send error" << std::endl;
}
}
private:
int _sockFd;
};
cpp
#include "UdpServer.hpp"
#include <memory>
#include <string>
#include <string.h>
std::string echo(const std::string& cData)
{
return "server : " + cData;
}
std::string command(const std::string& cData)
{
std::string ret;
FILE* fp = popen(cData.c_str(), "r");
if(fp == nullptr)
{
perror("popen");
exit(1);
}
while(true)
{
char buffer[1024];
memset(buffer, sizeof(buffer), 0);
char* ok = fgets(buffer, sizeof(buffer) - 1, fp);
if(ok == nullptr) break;
ret += buffer;
}
pclose(fp);
return ret;
}
int main()
{
std::unique_ptr<UdpServer> udpServer(new UdpServer());
udpServer->init();
udpServer->run(command);
return 0;
}
makefile:
bash
.PHONY:all
all:UdpServer client
UdpServer:main.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f UdpServer client
五.解释与运行
- 客户端:接收用户输入的字符串(可以是普通消息或系统命令),通过 UDP 发送给指定 IP 和端口的服务端,然后等待并打印服务端的响应。
- 服务端:以类的形式封装(UdpServer),监听指定端口,接收客户端的消息后,通过回调函数处理(默认是执行客户端发送的系统命令并返回执行结果,也可切换为简单回显),再将处理结果通过 UDP 返回给客户端。
运行结果:

这样,我们就可以远程输入命令了,xshell远程连接服务器就是这样类似的原理。
一些细节:

AF_INET指明使用ipv4协议,sock_DGRAM指明面向数据报的udp协议,第2个参数协议类型不用管。

绑定之前将自己的信息填入sockaddr_in这个数据结构中。AF_INET表明是网络socket编程,s_addr表明服务端的ip地址,调用inet _addr函数将它转换成32位数字并且转换为网络字节序列,也就是大端。htons也就是将short类型数据转换为网络字节序列,也就是大端。

倒数第一个参数len 既是输入型参数,也是输出型参数;倒数第二个参数是输出型参数。

网络转成主机的字节序,并且32位数字的ip转换为点分十进制字符串形式。
六.总结
我们还可以进行扩展。将这个服务弄成多人聊天。服务端用一个哈希和客户端的ip地址唯一标识一个客户端,管理起来之后就可以对每一条消息进行广播给每一个客户端了,客户端是多线程的,一个线程负责发送消息,一个线程负责接收消息,为了发送和接收不在一个终端进行,我们可以将接收线程里的标准输出或者标准错误重定向到/dev/pts/某某序号的这个文件夹,用dup2这个函数就可以做到,然后我们客户端就可以在一个终端发送消息,一个终端接收消息。