地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换;
字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
- 在APUE中, 明确提出inet_ntoa不是线程安全的函数
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
简单的TCP网络程序
API介绍
socket()
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol指定为0即可。
bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和addr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 addr所描述的地址和端口号;
- struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
sockaddr_in初始化
1、bzero(&local, sizeof(local));//将local清空一下
2、local.sin_family = AF_INET; // 域
3、local.sin_addr.s_addr =INADDR_ANY; // 零标识可以接收该主机任意IP接收 //的数据报,指定则只能接收对应IP
4、local.sin_port = htons(8888); // 端口转为网络字节序
- 1.将整个结构体清零;
-
- 设置地址类型为AF_INET;
-
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
-
- 端口号为SERV_PORT, 我们定义为8888;
listen()
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,.
- listen()成功返回0,失败返回-1;
accpet()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
connect()
#include <sys/types.h>
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1;
TCP网络程序实现(单进程版)
服务端
变量设计
int listensock_:用于创建套接字时的文件描述符
uint16_t port:用于记录服务端的端口号
string ip:用于记录服务端bind的ip地址
函数设计
构造函数
在构造函数时,将我们的变量初始化为默认值
const uint16_t defaultport = 8080;
const string defaultip = "0.0.0.0";
TcpServer(const uint16_t &port = defaultport, const string &ip = defaultip) :
listensock_(0), port_(port), ip_(ip)
{
}
初始化函数:InitServer()
在初始化函数中,我们需要利用socket函数创建我们的套接字,然后利用sockadd_in的结构体存储我们的服务端ip,port,和协议类型,利用bind进行绑定端口号,再利用listen函数将我们的套接字处于监听状态,监听是否有用户来连接我们的服务器。
注:lg仿函数是自定义的输出方法,和正常打印类似
void InitServer()
{
// 1.常见套接字
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
lg(Fatal, "socket error,errno:%d,errstr:%s", errno, strerror(errno));
exit(SOCKET_ERR);
}
lg(Info, "create socket success, listensock_: %d", listensock_);
// 2.bind绑定
// 2.1 准备服务端信息
struct sockaddr_in local;
inet_aton(ip_.c_str(), &local.sin_addr); // ip
local.sin_family = AF_INET;//协议类型
local.sin_port = htons(port_);。。端口号
// 2.2绑定
socklen_t len = sizeof(local);
int n = bind(listensock_, (struct sockaddr *)&local, len);
if (n < 0)
{
lg(Fatal, "bind error,errno:%d,errstr:%s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind socket success, listensock_: %d", listensock_);
// 3.监听
int s = listen(listensock_, 10);
if (s < 0)
{
lg(Fatal, "Listen error,errno:%d,errstr:%s", errno, strerror(errno));
}
lg(Info, "listen socket success, listensock_: %d", listensock_);
}
Start函数
我们调用start函数开始我们的接收消息。在接收消息之间我们需要利用accept函数来接收我们客户端的连接,然后返回一个文件描述符,这个文件描述符是用来进行网络通信的文件描述符,然后利用read和write系统调用即可和文件一样正常通信。
void Start()
{
while (true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);//接收连接的服务器
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
continue;
}
//将我们的客户端传来的sockaddr_int 结构体中的客户端的ip和port读出
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
Service(sockfd,clientip,clientport);//调用服务函数
close(sockfd);
}
}
Service函数
该函数中由于传入了网络的文件描述符,只需要我们来向文件一样来读写即可
void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
{
char buffer[4096];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
break;
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
break;
}
}
}
客户端
我们的服务端就很简单了,我们只需要利用socket创建网络套接字,然后利用connect连接我们的指定ip的对应的端口号程序。
cpp
int main(int argc,char*argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
//连接
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));
socklen_t len = sizeof(server);
int n = connect(sockfd,(struct sockaddr*)&server,len);
if(n <0)
{
perror("Connect :");
}
//开始发送数据和接收网络数据
string message;
while(true)
{
cout<<"Please Enter@ ";
getline(cin,message);
write(sockfd,message.c_str(),message.size());
char inbuffer[1024]= {0};
ssize_t n = read(sockfd,inbuffer,sizeof(inbuffer));
if(n>0)
{
inbuffer[n] = 0;
cout<<inbuffer<<endl;
}
}
return 0;
}
进程间关系
前台进程
在我们的Linux中我们通过**./**运行的文件我们可以称为前台进程,只有我们的前台进程才可以收到我们的键盘信号,也就是ctrl+c等信号,后台进程是收不到我们的键盘信号的,基于这个特性我们也可以将能够收到键盘信号的进程称之为前台信号。在一个Linux会话中只允许我们拥有一个前台进程,所以我们在运行前台进程后,我们的bash进程就来自动编程后台进程
后台进程
在Linux中我们可以通过在运行进程后面加一个 & 符就可以将我们的进程设置为后台进程,此时我们的后台进程就启动了,[1]代表我们的**任务编号,**后面就是我们的进程pid。

我们利用ps指令来查看一下我们的后台进程

PPID:父进程PID
PID:当前进程PID
PGID:当前进程组ID,进程组ID通常是我们任务组第一个进程
SID:会话ID,一次登录就会形成一次会话
当我们的会话退出之后,我们的后台进程有可能会被Linux领养变成了孤儿进程,此时由于我们之前的会话已经退出,所以他就会自成我们的一个会话,这就是我们的守护进程