Socket 编程 UDP
本章将函数介绍和代码编写实战一起使用。
IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和in_addr 表示之间转换;
字符串转 in_addr 的函数:
#include <arpa/inet.h>
int inet_aton(const char:*strptr,struct inaddr *addrptr);
int_addr_t inet_addr(const char *strptr);
int inet_pton(int family,const char *strptr,void *addrptr);
in_addr 转字符串的函数:
char *inet_ntoa(struct in addrinaddr);
const char *inet_ntop(int family,const void *addrptr, char *strptr,size t len);
查看OS所有UDP和进程信息:
netstat -naup
这个函数是创建一个套接字:
int socket(int domain, int type,int protocol);
第一个参数是套接字的域,就是确定是ipv4还是ipv6等待。
第二个是套接字的数据流类型。
第三个参数是协议类型。
返回值是:返回成功返回一个文件操作符,失败返回-1。
其实socket也就相当于创建了一个文件。
这个函数是绑定套接字:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
第一个参数是网络文件描述符
第二个参数是用哪一种套接字(让绑定过来的套接字实现哪一种功能)
将指定内存全部初始化为0的函数:
void bzero(void *s, size_t n);
第一个参数是传地址
第二个参数是缓冲区的大小
in_addr_t inet_addr(const char *cp);
让字符串转换成网络ip风格的四字节
接收信息函数:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
第二个参数为接收容器
第三个参数为信息长度
第四个参数为设置阻塞与非阻塞
第六个参数为套接字种类的长度
发送信息函数:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数和上一个函数差不多,只有最后一个参数是不需要取地址的
首先有一个代码的预备工作,实现一个日志附带文件打印功能的代码
//log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
Log log;
模拟服务器
#include "udpserver.hpp"
#pragma once
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
extern Log log;//声明变量log
enum{
SOCKET_ERR = 1,//套接字创建失败
BIND_ERR
};
uint16_t defaultport = 8080;//默认端口
//绑定端口号的时候要注意,很多端口都是被应用层协议固定使用[0,1023]这个区间:http:80 https:443等等
//建议使用1024以上,但也要注意,比如mysql:3306
string defaultip = "0.0.0.0";//默认ip,bind为0就是任意ip地址都可以进到服务器来
const int size = 1024;
class UdpServer
{
public:
UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip):sockfd_(0),port_(port),ip_(ip),isrunning_(false)
{
}
void Init()
{
//1.创建udp socket
sockfd_ = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd_ < 0)
{
log(Fatal,"socket create error, socket:%d",sockfd_);
exit(SOCKET_ERR);
}
log(Info,"socket create success, socket:%d",sockfd_);
//2.bind socket
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;//设置自己为IPV4
local.sin_port = htons(port_);//因为端口号需要给对方发送,所以必须要保证我的端口号是网络字节序列
local.sin_addr.s_addr = inet_addr(ip_.c_str());
//1.将string->uint32_t 2.必须是网络序列
//sin_addr里面还有一个成员,s_addr才是真实的本体
//local.sin_addr.s_addr=INADDR_ANY;也是绑定任意IP地址的方法
int n = bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
if(n < 0)
{
log(Fatal, "bind error, error: %d, err string:%s",errno, strerror(errno));
exit(BIND_ERR);
}
log(Info,"bind success, errno: %d, err string: %s",errno,strerror(errno));
}
void Run()
{
isrunning_ = true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
if(n < 0)
{
log(Warning,"recvfrom error,errno: %d,err string:%s",errno,strerror(errno));
continue;
}
string info = inbuffer;
string echo_string = "server echo#" + info;
sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
}
}
~UdpServer()
{
if(sockfd_ > 0) close(sockfd_);
}
private:
int sockfd_;//网络文件描述符
string ip_;//服务器iP地址
uint16_t port_;//服务器进程的端口号
bool isrunning_;//服务器是否在运行
};
#include "log.hpp"
#include "udpserver.hpp"
void Usage(string proc)
{
cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run();
return 0;
}
客户端
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;
void Usage(string proc)
{
cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
cout << "socket error" << endl;
exit(1);
}
//客户端需要绑定,但是不需要显示绑定,由OS自己选择
//为了防止进程端口出现冲突
//OS什么时候给我绑定的呢?是在首次发送数据的时候
string message;
char buffer[1024];
while(true)
{
cout << "Please Enter@ ";
getline(cin,message);
//1.数据2.发给谁
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);
if(s > 0)
{
buffer[s] = 0;
cout << buffer <<endl;
}
}
close(sockfd);
return 0;
}
Socket 编程 TCP
测试服务器工具,指定服务器远程登陆:
telnet 127.0.0.1 端口号
127.0.0.1表示本地环回。
第一个函数也是将字符串的ip转换成网络四字节的ip。
因为TCP是面向连接的,服务器比较被动,一直处于等待链接到来的状态,所以用监听的方式查看是否有客户端到来。
这个函数是将套接字设置监听状态:
int listen(int sockfd, int backlog);
第二个参数等后面TCP协议在进行解释
接收消息函数,并获知哪个客户端连接上了自己:
int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);
这里返回值也是一个套接字,那么我们第一个参数也是套接字,有什么区别呢?
我们输入的套接字就相当于饭店外面的接待员,并不参与真正的服务当中,返回值的套接字才是真正的服务员,服务被接待过的客人。
也就是说,第一个参数的套接字仅仅是帮助我们获取新连接的工具人。
通过指定的套接字发送连接。
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
这个客户端与服务器的程序还是要用到上面log的代码:
服务器
#pragma once
#include "log.hpp"
#include <memory>
#include <sys/socket.h>
#include <cstdlib>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
extern Log log;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10;
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError
};
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
{}
public:
int sockfd;
string clientip;
uint16_t clientport;
TcpServer *tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port,const string &ip = defaultip):listensock_(defaultfd),port_(port),ip_(ip)
{
}
void InitServer()
{
listensock_ = socket(AF_INET,SOCK_STREAM,0);
if(listensock_ < 0)
{
log(Fatal,"create socket,errno: %d,errstring: %s",errno,strerror(errno));
exit(SocketError);
}
log(Info,"create socket success, sockfd:%d",listensock_);
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(),&(local.sin_addr));
if(bind(listensock_,(struct sockaddr*)&local,sizeof(local)) < 0)
{
log(Fatal,"bind error, errno: %d, errstring:%s",errno,strerror(errno));
exit(BindError);
}
if(listen(listensock_,backlog)<0)
{
log(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
log(Info, "listen socket success, listensock_: %d", listensock_);
}
static void *Routine(void *args)
{
pthread_detach(pthread_self());//分离状态不用让主线程去等待,互不影响
ThreadData *td = static_cast<ThreadData *>(args);
td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
delete td;
return nullptr;
}
void Start()
{
log(Info, "tcpServer is running....");
for(;;)
{
//1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_,(struct sockaddr*)&client,&len);
if(sockfd < 0)
{
log(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //获取一个失败不必推出,再次进行下一个获取即可
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
//2. 根据新连接来进行通信
log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
//单进程版本,无法让多个客户端进行连接
/*Service(sockfd, clientip,clientport);
close(sockfd);*/
//多进程版,让子进程去处理客户端,父进程继续向下执行。创建多个进程就能连接多个客户端
/*pid_t id = fork();
if(id == 0)
{
//child
close(listensock_);//这个是父进程用的fd,防止误操作
if(fork() > 0) exit(0);//因为是阻塞等待,所以让子进程再创建子进程去处理客户端
Service(sockfd, clientip, clientport); //孙子进程来处理客户端,system 领养
close(sockfd);
exit(0);
}
close(sockfd);//这里必须关闭,因为sockfd已经交给子进程处理了,父进程不需要在管理了,不然父进程的文件描述符会越用越少
// father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;*/
//多线程版
ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
}
void Service(int sockfd,const string& clientip,const uint16_t &clientport)//因为是面向字节流的,所以用read和write对网络文件进行读写即可
{
char buffer[4096];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
cout << "client say# " << buffer << endl;
string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)//客户端退出,就会关闭套接字
{
log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
break;
}
else
{
log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
break;
}
}
}
~TcpServer()
{
}
private:
int listensock_;
string ip_;
uint16_t port_;
};
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(const string &proc)
{
cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< 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]);
//TCP方式的客户端bind实在什么时候呢?
//是在connect的时候OS自动绑定
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
cerr << "connect error..., reconnect: "<< endl;
}
while(true)
{
string message;
cout << "Please Enter# ";
getline(cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
cerr << "write error..." << endl;
}
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
cout << inbuffer << endl;
}
}
close(sockfd);
return 0;
}
客户端
#include "tcpserver.hpp"
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(UsageError);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
tcp_svr->InitServer();
tcp_svr->Start();
return 0;
}
守护进程
两个人在使用一个服务器,那么服务器就会生成两个“会话”(session),会话里面包含了命令行解释器(bash),和多个进程。
眼下,两个bash都是前台进程,其他的都是后台进程,并且一个会话当中只能有一个前台进程。
无论前台还是后台进程都可以向显示器进行打印,但是能用键盘的只有前台。(比如说将bash变成后台,另一个进程变成前台进程,Ctrl+C就能让这个进程停止,然后将bash换回到前台)——谁拥有键盘文件谁就是前台。
如何将进程后台运行呢?
只需要在启动可执行文件的时候加上一个&即可。
如果想让进程变成前台需要
fg+任务号
用jobs指令来查看后台任务号。
如果某一个前台进程被暂停之后放到后台了,想让这个后台继续运行:
bg+任务号
进程间的关系
这里的PGID是进程组的ID,SID是会话的ID。
会话也是要被OS组织管理的。
第一个进程PID和PGID是一样的,说明这个进程自成一组。
剩下三个PGID一样说明他们三个是一组。(一般来说,第一个进程是组长)
组长是组内中PID和PGID相同的进程。
平时所有任务其实都是进程组在完成!
也就是说,前台进程和后台进程其实并不正确,应该叫做前台任务和后台任务!
那么SID的ID是谁呢?
其实就是bash。
如果客户端退出了呢?
TTY全变成了?也就是说跟终端无关了。
TPGID变成了1。
并且退出的客户端的进程全都被OS给领养了。
也就是说这些进程收到了客户端登录和退出的影响。
如果不想让这些进程受到客户端的影响,那么这就叫守护进程化。(也叫精灵进程)
注销
windowsOS党总有一个操作叫做注销,注销就是将整个会话给关闭。
当重新登录的时候就相当于重新创建一个会话。
守护进程
如何进行守护进程化呢?
那就是让一个会话当中的某个进程脱离当前会话,自成一个会话,上一个会话进行销毁也就和这个进程无关了。
函数接口
#include <unistd.h>
pid_t setsid(void);
谁调用这个函数谁就被设置成为守护进程,成功返回一个新的pid,失败返回-1.
但是这个函数不会让这个进程成为新会话的组长。
可是新的会话只有这个进程,那怎么办呢?只要不让这个进程是第一个进程就好了。
if(fork()>0) exit(0);
srtsid();
所以守护进程的本质也是孤儿进程。
如果程序生成一个守护进程(以服务器举例),分为以下几个步骤:
1.忽略部分异常信号
2.将自己变成独立会话
3.更改当前调用进程的工作目录
4.标准输入输出错误不要在打印到屏幕上,重定向到/dev/null(也可以放在一个文件里形成文件的日志)
这样就能让一个服务器在后台持久运行了。
注意:守护进程命名习惯是后面以d为结尾。
让进程和以上效果相同的函数:
第一个参数是设置为0是将工作目录设置为根目录,否则就是当前目录,
第二个参数是设置为0是将标准输入输出错误重定向到/dev/null。
TCP简单的特性
三次握手与四次挥手
TCP会三次握手来进行链接的建立:
通过四次挥手进行释放:
注意:TCP是全双工的。(可以互相通信)
那么为什么不会相互收到影响呢?
因为在两个客户端当中,双方网络文件的上层都有自己的两种缓冲区,下层也是,所以不会冲突。(双方资源是隔离的)
也就是说我们上面用的read和write都是在对网络上下的两个缓冲区之间进行拷贝。
但TCP是面向字节流的,我们如何保证都上来的是一个完整的报文?
在用read和write的时候,TCP会有一个传输控制协议!
什么时候发,发多少,出错了如何解决?
也就是说write写的时候,其实从自己的缓冲区发送到网络的缓冲区就直接返回了,我们并不知道到底有没有发送到对方手里,因为这是TCP决定的。(其实就是给了OS,因为TCP也是在OS当中实现的,也就是TCP网络模块)
同理,read也是一样的。
所以这就需要协议定制,序列化和反序列化。