【Linux】Socket编程预备及UDP
目录
🐼理解网络通信的本质
🐼认识端口号
🐼网络字节序
🐼Socket套接字的种类
🐼Udp常用接口
🐼EchoServer
🐼DictServer
🐼ChatServer
🐼理解网络通信的本质
这里要思考一个问题: 数据传输到主机是目的吗? 不是的。 因为数据是给人用的。 比如: 聊天是人在聊天, 下载是人在下载, 浏览网页是人在浏览?
但是人是怎么看到聊天信息的呢? 怎么执行下载任务呢? 怎么浏览网页信息呢? 通过
启动的 qq, 迅雷, 浏览器。
而启动的 qq, 迅雷, 浏览器都是进程。 换句话说, 进程是人在系统中的代表, 只要把
数据给进程, 人就相当于就拿到了数据。
所以: 数据传输到主机不是目的, 而是手段。 到达主机内部, 在交给主机内的进程,
才是目的。
但是系统中, 同时会存在非常多的进程, 当数据到达目标主机之后, 怎么转发给目标
进程? 这就要在网络的背景下, 在系统中,全网内, 标识主机的唯一性。
🐼认识端口号
✅为什么要有端口号?
我们已经有了IP地址来表示全网内唯一的主机,并且我们知道,网络通信,本质上是进程在通信,谁要求做的?人。但是,如果标明全网内的唯一的进程呢?我们要找到这个进程,才能通信啊!
✅如何做?通过端口号。
端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数,uint16_t;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
一个端口号只能被一个进程占用.
所以IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
如图:

综上, IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的一个网络进程
IP+Port 就能表示互联网中唯一的一个进程所以, 通信的时候, 本质是两个互联网进程代表人来进行通信, {srcIp,srcPort, dstIp, dstPort}这样的 4 元组就能标识互联网中唯二的两个进程,
解决了"数据是谁发的, 要发给谁"的问题。
我们把ip+port叫做socket(套接字)
🚩注意:
- 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的,也就是我们可以使用的端口
- 端口号 != 进程ID,一个进程可以绑定多个端口号。而一个端口号只能被一个进程绑定。进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这样做。
之前,我们从os系统的角度认识了网络。网络本身是属于OS的,既然属于os,那么就一定是内核态的,我们想访问网络必须通过系统调用。如图:

🐼网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

小端我们仅需记忆为"小小小",即低地址处,存放低位字节,是小端。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。 在网络数据流中统一是大端地址。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数(IP),s表示16位短整数(port)。
- 例如:htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
通过以上接口,我们再也不区分是大小端了,只需要做网络字节序和主机字节序的转换即可
🐼Socket套接字的种类
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,其种类有很多,各种网络协议的地址格式不同。
只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容。socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收 IPv4, IPv6,struct sockaddr不就是基类吗,而struct sockaddr_in和struct sockaddr_un不就是子类吗。通过基类的指针和子类的重写不就可以形成多态吗?这也是C语言形成的多态。但是为什么不用void*强转呢,因为网络接口出来的时候还没有void*。但是我们真正在基于 IPv4 编程时, 使用的数据结构sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP 地址,sockaddr_in结构如图:

🐼Udp常用接口
下面我们实现一个客户端说什么。我就给客户端返回什么的echoserver,目的就是熟悉接口。
在这之前,我们下面来介绍三个接口:
第一个接口,创建socket:

需要注意:
如果是IPV4通信,第一个参数在常常设置为AF_INET.
如果是UDP通信,是面向数据报的,第二个参数常常设置为SOCK_DGRAM
返回值竟然是一个文件描述符。所以,未来的网络通信,也是基于文件来通信的,这也就是为何Linux一切皆文件的原因。把网络在文件系统中打开。
// 创建套接字_sockfd = socket(AF_INET /*ipv4*/, SOCK_DGRAM /*面向数据报,无连接的、不可靠的*/, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "create sockfd error";exit(1);}LOG(LogLevel::INFO) << "create sockfd success, sockfd: " << _sockfd;
第二个接口,绑定bind( bind a name to a socket)

bind第一个参数是我们创建socket已经打开的fd
bind第二个参数是我们需要填入的套接字地址,如果是网络套接字,我们一般使用这个socket地址
其中socket_in的第一个字段sin_family 表明是网络套接字还是域间套接字(可以展开宏看看),关于地址族。第二个字段sin_port为套接字的端口号,sin_addr为套接字的ip地址,最后一个为填充字段
bind第三个参数为该套接字地址的长度。
// 绑定bindstruct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET; // 协议族local.sin_port = htons(_port);// 为什么不绑定固定ip?什么叫做接受任意IP bind ? 不明确具体IP,只要是发给我的主机的,对应端口号// 通通可以收到!!!这就是服务器的特性!local.sin_addr.s_addr = htonl(INADDR_ANY);socklen_t len = sizeof(local);int n = bind(_sockfd, (const sockaddr *)&local, len);if (n < 0){LOG(LogLevel::FATAL) << "bind sockfd error";exit(2);}LOG(LogLevel::INFO) << "bind sockfd success, sockfd: " << _sockfd;
recvfrom

前三个参数表明,我要从哪个套接字中读,读少字节,读的内容存在哪。
主要关注第四个参数。由于我们从socket读取信息,但是我们并不知道,我们从对端谁那里读取的,我们收到了消息,可能后面还要给对方发送消息,我就必须要知道对方的IP+port。所以第四个参数是一个输出型参数,可以帮助我们获取对方的socket,即IP+port。
需要注意的是:recvfrom是为无连接的 UDP 套接字设计的标准函数。它的主要特点是不仅能接收数据,还能同时告诉你数据是谁(哪个地址和端口)发来的。即如果我关心对端是谁,像udp不需要accept,因为是无连接的,没有connect,但是又关心对端的信息,就需要使用recvfrom。
返回值,如果返回0,表明对端关闭连接,如果<0,表明读取错误。否则,就是读取的数据个数。
sendto

一般recvfrom和sendto配套使用。
🐼EchoServer
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h> // 需要 for close()
#include <cstdlib> // 需要 for exit()
#include <errno.h> // 需要 for errno
#include <string> // 需要 for std::string
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Logger.hpp"class EchoServer
{const static uint16_t defaultsockfd = -1;private:int _sockfd;uint16_t _port;// uint32_t _ip; // Server不建议绑定固定ipbool _is_running;private:void Init(){// 创建套接字_sockfd = socket(AF_INET /*ipv4*/, SOCK_DGRAM /*面向数据报,无连接的、不可靠的*/, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "create sockfd error";exit(1);}LOG(LogLevel::INFO) << "create sockfd success, sockfd: " << _sockfd;// 绑定bindstruct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET; // 协议族local.sin_port = htons(_port);// 为什么不绑定固定ip?什么叫做接受任意IP bind ? 不明确具体IP,只要是发给我的主机的,对应端口号// 通通可以收到!!!这就是服务器的特性!local.sin_addr.s_addr = htonl(INADDR_ANY);socklen_t len = sizeof(local);int n = bind(_sockfd, (const sockaddr *)&local, len);if (n < 0){LOG(LogLevel::FATAL) << "bind sockfd error";exit(2);}LOG(LogLevel::INFO) << "bind sockfd success, sockfd: " << _sockfd;}public:EchoServer(uint16_t port) : _port(port), _sockfd(defaultsockfd), _is_running(false){Init();}void Start(){_is_running = true;while (_is_running){char buff[1024];struct sockaddr_in peer;socklen_t len;// 没有客户端的消息,在这里阻塞ssize_t n = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&peer, &len);if (n < 0){LOG(LogLevel::ERROR) << "recvfrom error: " << strerror(errno);continue;}else if (n == 0){LOG(LogLevel::DEBUG) << "收到空数据报";continue;}else{buff[n] = 0;std::string echo_string = "server say#";echo_string += buff;ssize_t m = sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&peer, len);(void)m;}}_is_running = false;}void Stop(){_is_running = false;}~EchoServer(){if (_sockfd != defaultsockfd){close(_sockfd);}}
};
✅细节一:服务器的ip不能绑定为固定IP.服务器需要为所有网络接口上的客户端提供服务,而不仅仅是某个特定IP的客户端。即在网络编程中, 当一个进程需要绑定一个网络端口以进行通信时, 可以使用INADDR_ANY 作为 IP 地址参数。 这样做意味着该端口可以接受来自任何 IP 地址的连接请求, 无论是本地主机还是远程主机。 例如, 如果服务器有多个网卡(每个网卡上有不同的 IP 地址) , 使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网卡/IP 地址上面获取的
所以最佳实践: local.sin_addr.s_addr = htonl(INADDR_ANY);
✅细节二:udp为无连接的,面向数据报的。当没有收到消息时,recvfrom就在其阻塞,当其返回值n == 0,这就表明收到了客户端的空报文,可以选择回复或者忽略。和tcp不同的是,而tcp当n == 0,表明对端断开连接了,就需要关闭连接了,释放资源。这就是udp无连接的体现。
✅细节三:客户端需要bind吗?不需要。理由有三。
1.客户端让操作系统自动处理底层细节,专注于发送/接收数据
2.操作系统自动分配临时端口
3.可能与其他程序冲突(如多个客户端运行在同一机器)!!!!!!
即客户端不应该显示绑定端口号,os系统会自动帮我们在内核态随机绑定端口号。后续我们可以验证这个随机性。
✅细节四:UDP 协议支持全双工, 一个 sockfd, 既可以读取, 又可以写入,我们已经验证了对于客户端和服务端同样如此
🐼DictServer
下面我们基于EchoServer,给我们的服务器对外提供字典服务
// 上层逻辑
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <boost/algorithm/string.hpp>
#include "Logger.hpp"
const static std::string defpath = "./dict.txt";
class UtilBoost
{
public:static void CuttingSubstrings(std::vector<std::string> *out, const std::string &origin_string, const std::string &sep){boost::split(*out, origin_string, boost::is_any_of(sep), boost::algorithm::token_compress_on);}
};
class Dictionary
{
private:bool LoadDictionary(){std::ifstream in(_path);if (!in.is_open()){return false;}std::string line;// getline 1:默认不带'\n', 2. 重载了返回值while (getline(in, line)){std::vector<std::string> words;UtilBoost::CuttingSubstrings(&words, line, ": ");std::string eng = words[0];std::string chin = words[1];if (eng.empty() || chin.empty()){LOG(LogLevel::WARNING) << "插入单词错误,错误点: " << line;continue;}_dict.insert(std::make_pair(words[0], words[1]));}LOG(LogLevel::INFO) << "加载词库成功";return true;}public:Dictionary(const std::string &path = defpath) : _path(path){assert(LoadDictionary());}~Dictionary() {}public:std::string Translate(const std::string &inword){auto iter = _dict.find(inword);if (iter == _dict.end()){return "unknown";}return iter->first + "->" + iter->second;}private:std::string _path;std::unordered_map<std::string, std::string> _dict;
};// 服务器
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h> // 需要 for close()
#include <cstdlib> // 需要 for exit()
#include <errno.h> // 需要 for errno
#include <string> // 需要 for std::string
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Logger.hpp"
using callback_t = std::function<std::string (const std::string&)>;
class DictServer
{const static uint16_t defaultsockfd = -1;private:int _sockfd;uint16_t _port;// uint32_t _ip; // Server不建议绑定固定ipbool _is_running;callback_t _cb; // 回调到上层的回调函数
private:void Init(){// 创建套接字_sockfd = socket(AF_INET /*ipv4*/, SOCK_DGRAM /*面向数据报,无连接的、不可靠的*/, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "create sockfd error";exit(1);}LOG(LogLevel::INFO) << "create sockfd success, sockfd: " << _sockfd;// 绑定bindstruct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET; // 协议族local.sin_port = htons(_port);// 为什么不绑定固定ip?什么叫做接受任意IP bind ? 不明确具体IP,只要是发给我的主机的,对应端口号// 通通可以收到!!!这就是服务器的特性!local.sin_addr.s_addr = htonl(INADDR_ANY);socklen_t len = sizeof(local);int n = bind(_sockfd, (const sockaddr *)&local, len);if (n < 0){LOG(LogLevel::FATAL) << "bind sockfd error";exit(2);}LOG(LogLevel::INFO) << "bind sockfd success, sockfd: " << _sockfd;}public:DictServer(uint16_t port, callback_t cb) : _port(port), _sockfd(defaultsockfd), _is_running(false), _cb(cb){Init();}void Start(){_is_running = true;while (_is_running){char buff[1024];struct sockaddr_in peer;socklen_t len = sizeof peer; // 必须要加初始化,Bug// 没有客户端的消息,在这里阻塞ssize_t n = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&peer, &len);if (n < 0){LOG(LogLevel::ERROR) << "recvfrom error: " << strerror(errno);continue;}else if (n == 0){LOG(LogLevel::DEBUG) << "收到空数据报";continue;}else{// 只需要在这里逻辑改动调用上层服务即可buff[n] = 0;std::string tranlated_word = _cb(buff); // 上层服务ssize_t m = sendto(_sockfd, tranlated_word.c_str(), tranlated_word.size(), 0, (const sockaddr *)&peer, len);(void)m;}}_is_running = false;}void Stop(){_is_running = false;}~DictServer(){if (_sockfd != defaultsockfd){close(_sockfd);}}
};// 调用
#include "Dictionary.hpp"
#include "DictServer.hpp"
#include <memory>
void Usage(const std::string& msg)
{printf("Usage: %s + port\n", msg.c_str());
}
int main(int argc, char* argv[])
{EnableConsoleLogStrategy();if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<Dictionary> dsvr = std::make_unique<Dictionary>();std::unique_ptr<DictServer> esvr = std::make_unique<DictServer>(port, [&dsvr](const std::string& word)->std::string{return dsvr->Translate(word);});esvr->Start();return 0;
}
这里只提供了英语->汉语,后期也可以实现英语到汉语的互译。可以对上述接口进行封装。也可以接入多线程,将字典的翻译服务交付给多线程。
🐼ChatServer
下面我们基于EchoServer,把我们的服务器改成聊天室,服务器不负责转发消息,将获取上来的客户端[ip, port, message, sockfd]打包交给后端的线程池中,将转发消息打包成任务,让后端的线程池转发消息。为了管理获取上来的对端的用户的地址信息,我们新增了InetAddr模块。以及将信息分发给所有用户的Route模块,大致流程如图:

下面是一些重要新增的模块的参考代码
InetAddr模块
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
private:// 网络地址struct sockaddr_in _addr;// 主机风格的地址std::string _ip;uint16_t _port;private:void Host2Net(){memset(&_addr, 0, sizeof _addr);_addr.sin_family = AF_INET; // 协议族_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = inet_addr(_ip.c_str());}void Net2Host(){_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);}public:// 主机转网络,缺省默认服务器接收任意IPInetAddr(uint16_t port, const std::string& ip = "0.0.0.0") : _port(port), _ip(ip){Host2Net();}// 网络转主机InetAddr(const sockaddr_in& addr) : _addr(addr){Net2Host();}struct sockaddr_in* Addr(){return &_addr;}socklen_t len(){return sizeof _addr;}bool operator==(InetAddr& user){return (this->_ip == user._ip && this->_port == user._port);}std::string SocketToString(){return "[ip: " + _ip + "]-[port: " + std::to_string(_port) + "] "; }~InetAddr() {}
};
route.hpp模块
#pragma once
#include <iostream>
#include <string>
#include <vector>#include "InetAddr.hpp"
#include "Logger.hpp"
class Route
{
private:bool IsExist(InetAddr &client){for (auto user : _online_users){if (user == client){return true;}}return false;}void Adduser(InetAddr &client){if (IsExist(client))return;_online_users.emplace_back(client);}void SendMessageToAll(const std::string &msg, int sockfd){// 都向Server的sockfd中写for(auto user: _online_users){LOG(LogLevel::DEBUG) << "route [" << msg << "] to: " << user.SocketToString(); std::string info = user.SocketToString(); info += "# ";info += msg; // XXXX-PORT# 你好sendto(sockfd, info.c_str(), info.size(), 0, (const sockaddr*)user.Addr(), user.len());}}void DelUser(InetAddr& user, const std::string& msg){// 暂时if(msg != "q") return ;auto iter = _online_users.begin();for(; iter < _online_users.end(); iter++){if(*iter == user){LOG(LogLevel::DEBUG) << user.SocketToString() << "已经被移除";_online_users.erase(iter);break;}}}public:Route(){}// 群聊void RouteMessageToAll(const std::string &msg, InetAddr &client, int sockfd){// 添加用户(里面自已有判读)Adduser(client);// 统一发送消息SendMessageToAll(msg, sockfd);// 删除用户(里面自已有判读)DelUser(client, msg);}// 单聊// void RouteMessageToOne(const std::string& msg, InetAddr& client, int sockfd)// {// }~Route() {}
private:std::vector<InetAddr> _online_users; // 这里默认就是表示在线用户,可扩展。比如用户离线和在线
};
chatserver.cpp调用逻辑
#include "ThreadPool.hpp"
#include "Route.hpp"
#include "ChatServer.hpp"
#include <memory>
void Usage(const std::string &msg)
{printf("Usage: %s + port\n", msg.c_str());
}
using task_t = std::function<void()>;
int main(int argc, char *argv[])
{// 1. 启动日志EnableConsoleLogStrategy();if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);/*std::unique_ptr<Route> r = std::make_unique<Route>();//服务器只负责接收消息,转发路由推送消息应该交给后台线程----单线程std::unique_ptr<ChatServer> esvr = std::make_unique<ChatServer>(port,[&r](const std::string& msg, InetAddr& client, int sockfd){r->RouteMessageToAll(msg, client, sockfd);});*/// 2. 路由功能std::unique_ptr<Route> r = std::make_unique<Route>();// 3. 启动线程池ThreadPool<task_t> *tp = ThreadPool<task_t>::GetInstance();tp->Start();// 4. 启动聊天服务器// 写法一:使用bind,返回类型为匿名函数用function接收// std::unique_ptr<ChatServer> esvr = std::make_unique<ChatServer>(port,[&r, &tp]// (const std::string &message, InetAddr &client, int sockfd){// task_t task = std::bind(&Route::RouteMessageToAll, r.get(), message, client, sockfd);// tp->Enqueue(task); // 写法2// });// 写法二:看调用层数情况,套lambda咯(2层)只需要用function接受lambda匿名对象std::unique_ptr<ChatServer> esvr = std::make_unique<ChatServer>(port,[&r, &tp](const std::string &message, InetAddr &client, int sockfd){task_t task = [&r, &message, &client, &sockfd](){r->RouteMessageToAll(message, client, sockfd);};tp->Enqueue(task);});esvr->Start();tp->Wait();return 0;
}
✅细节一:什么时候接入线程池,为什么要接入线程池?
刚开始,我们的服务器(主线程)不仅负责接受客户端的请求,也要负责将其转发给所有用户,这就是强耦合了,转发消息是一个很复杂的过程,主要是一些用户逻辑,比如增删查改,再转发,时间较长。这时,我们接入线程池,将路由转发包装成任务,入线程池阻塞队列,让多线程领一个任务去完成,我们的chat_server服务器只负责接受,不负责转发。实现了解耦!可以通过主函数调用逻辑在来理解。
✅细节二:我们的代码对于_online_users的访问安全吗?
不安全。当多线程同时访问_online_users,进行写操作,就会引发线程安全问题。所以后面可以加锁保护_online_users。实现解耦的好处就是扩展起来方便,小伙伴后面可以扩展~
✅细节三:如果我们使用Windows的客户端访问我们的服务器,可以吗?
当然可以。但是我们需要现在云服务器开放对应的端口号。我们需要手动开启端口号,允许外部访问。
🐼网络命令
✅我们可以使用ping命令来测试网络的连通性
比如ping www.baidu.com
通过ping命令可以测试比如我们的服务器是否是因为网络问题导致的异常。
可以带-c选项表示测试几个数据包。
ping -c2 www.baidu.com,如果我们仅想测试网络连通性,-c1即可。
✅我们可以使用netstat 来查看网络状态的重要工具.
u (udp)仅显示 udp 相关选项
a (all)显示所有选项, 默认不显示 LISTEN 相关
n 拒绝显示别名, 能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示 tcp 相关选项
✅watch -n 1 netstat -nltp
通过watch来轮询观测一个命令的执行结果
比如watch -n 1 cmd表示一秒查询一次cmd命令
✅我们可以使用pidof在查看服务器的进程 id 时非常方便
当我们的服务器进程变成后台进程或者守护进程时,使用
pidof server 查看进程pid,通过管道交给kill -9
组合命令
pidof server | xargs kill -9
来杀掉一个指定进程
