我们编写的程序为用户层程序,要用的接口都是系统调用接口,可以理解为在用户层自定义协议,其在应用层,使用的是传输层的接口。后续学习的一般为传输层接口。
在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。
跨网络传输的第一件工作就是保证准确的找到目标主机。IP标定全公网内唯一一台主机。目的IP保证数据往哪里发。除了把请求交给远端服务器外,也要让远端服务器能找到自己。
电脑打开浏览器,硬件是承担通信的载体,实际通信的是手机上搭载的软件和服务器软件,浏览器本身是一个硬盘上的程序,运行成为进程,所谓套接字本质就是进程间通信。
打开手机虽然服务端看不到,要想使用微信得先打开微信客户端,打开后从系统层面来说就是进程,在远端也有一个对等的服务器(软件层面人编写的服务器)来提供服务。
一台电脑上也不止一个进程同时在跑,我们的IP地址主要负责的是把数据从一台主机硬件传送到另一台主机硬件,如何确定服务器主机获得的数据包给哪个进程?
因此还要通过某种方式标识主机上的特定进程。端口号用来标识特定一台主机上的唯一一个进程。
客户端有IP地址,服务端有IP地址,客户端特定一个进程有端口号,服务端特定一个进程也有端口号。
IP+PORT(端口号)标识的是全网内唯一一个进程,通常将IP+端口号叫做套接字。socket套接字本质是进程间通信。
端口号(port)是传输层协议的内容。
之前在学习系统编程的时候,pid表示系统内唯一一个进程;此处我们的端口号也是唯一表示一个进程。那么这两者之间是怎样的关系?
不是所有进程都是网络进程,因此不是所有进程都需要端口号,但是所有进程在系统层面被管理都需要唯一编号。端口号标定在众多网络进程中要找的那个进程。
PID是每个进程都要有,当该进程是网络进程需要指派端口号。
因此一个进程可以既有PID又有端口号,也可以只有PID没有端口号。
另外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定。
举个例子:10086的人工客服接听时说的工号就是端口号,该客服自身的身份证是进程ID。
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述"数据是谁发的, 要发给谁"。"数据是谁发的"是目的主机返回数据使用的。
光光找到IP地址对应的主机没有用,还要找到对应接收的进程。
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
后面我们再详细讨论TCP的一些细节问题:传输层协议、有连接、可靠传输、面向字节流。
TCP协议保证可靠性,丢包,对方来不及接收,网络拥塞等问题,导致TCP协议比较复杂,没有其他协议那么高效。
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;
后面再详细讨论:传输层协议、无连接、不可靠传输、面向数据报。
只负责将报头往下塞,设计比较简单,往往会比较高效一些。
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
规定默认网络中的数据是大端的。
大小端的判别:小小小——数据有高权值位和低权值位之分,内存有高地址和低地址之别,低权值的数据放在低地址称为小端。
为什么网络要采用大端序列
比如说要发一个字符串"abcdef",到对端之后想从开始位置进行计算,采用大端传送可以边接收边计算。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
htonl
:host-to-net-long。从主机序列转成网络序列
#include
uint32_t htonl(unint32_t hostlong);
uint16_t htons(unint16_t hostshort);
uint32_t ntohl(unint32_t netlong);
uint16_t ntohs(unint16_t netshort);
其中端口号是16位。
IP地址:“XXX.XXX.XXX.XXX”,网络中尽可能用较少数据传输我们的内容,每个区域(0~255)只占1个字节,4个区域要用四个字节。
00000000 00000000 00000000 00000000。IP传输的时候不用传.
。因此使用32_t
进行转化IP地址。
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int socket(int domain, int type, int protocol)
:
进程间通信是需要通信资源的,最重要的一个资源是网卡。linux下一切皆文件,那么网卡要和文件系统相关挂接。
网卡,硬盘,显示器,键盘是硬件外围设备,操作系统要进行管理,先描述再组织,linux内核中包含struct device
去描述所有设备,网络设备一般为net device
。
创建套接字的过程目前来说就是在底层创建通信需要的网卡相关的资源,第二个是需要和文件系统挂钩的文件系统相关资源。
一个进程可以打开一个套接字,可以和打开文件类似打开多个套接字,系统中如果存在多个套接字,操作系统就要先描述再组织进行管理,struct socket
。我们重点关心和文件怎么关联。
进程和套接字怎么产生关联?通过文件描述符表关联起来。
进程启动,得到文件描述符,通过文件描述符找到对应的files_struct
,files_struct
中有一个void *private_data
的字段,指向socket
结构,至此,完成对应的通信。socket
中有proto_ops
,其内部存函数指针,将来不同的套接字种类指向不同的套接字方法,这叫多态。
struct socket
{
socket_state state; // socket state
short type ; // socket type
unsigned long flags; // socket flags
struct fasync_struct *fasync_list;
wait_queue_head_t wait;
struct file *file;
struct sock *sock; // socket在网络层的表示;
const struct proto_ops *ops;/*操作指针,里面都是函数指针*/
}
struct socket结构体的类型
enum sock_type
{
SOCK_STREAM = 1, // 用于与TCP层中的tcp协议数据的struct socket
SOCK_DGRAM = 2, //用于与TCP层中的udp协议数据的struct socket
SOCK_RAW = 3, // raw struct socket
SOCK_RDM = 4, //可靠传输消息的struct socket
SOCK_SEQPACKET = 5,// sequential packet socket
SOCK_DCCP = 6,
SOCK_PACKET = 10, //从dev level中获取数据包的socket
};
int bind(int sockfd, const struct sockaddr *address,socklen_t address_len);
之前打开的普通文件关联的是硬盘中的某个文件,绑定本质是把内存文件和网络信息关联起来。将Server自己的ip地址和端口号绑定起来,方便其他用户找到。在系统层面来说,文件信息有了,网络信息有了,绑定是将两者产生关系。
服务器需要绑定,客户端不需要绑定。
参数 | 含义 |
---|---|
返回值 | 表示绑定成功与否 |
int sockfd | 套接字 |
const struct sockaddr* address | 给套接字绑定的ip+端口,初始化底层函数指针指向不同的标准方法 |
socklen_t address_len | 传入的长度 |
注意直接写的ip和端口号是用户层的,实际上套接字要在内核层创建用来,绑定也是要内核层的。要把接口参数继续往下传。
讲一切皆文件的时候,学习了文件结构体里包含了大量的函数指针。绑定主要做两件事:填充当前服务器的ip地址和端口号;不同套接字的操作方法不一样,初始化底层的函数指针,指向不同的标准方法。
服务端要bind,而客户端都不需要bind,但是需要IP和port,原因是什么?
为什么服务器要bind?
服务器要强制把端口占住。服务器也会出现冲突,但是都是公司内部,可以协商,而且有利于将服务和端口形成强相关,比如http:80。服务器要一对多,因此其端口必须众所周知,明确的。
而客户端可以进行bind,但是不需要。因为一般server端的ip和port不能轻易更改,必须是确定的,众所周知的。比如http:80,https:443,ssh:22,MySQL:3306。服务器和客户端的关系是1对n,一旦修改了客户端就找不到了。
并且客户端都不需要bind,一个客户来说有多种客户端并且会同时跑,因此如果绑定那么不同的公司就要互相协商规定各自使用什么端口。如果要绑定一个端口号,而这个端口号被另一个客户端给绑定了,绑定就会出错。因为一个端口号只能和一个进程相关。
总的来说,客户端都不需要bind:
但是需要IP和port:client udp,recv和send,系统会自动进行ip和端口号的绑定,操作系统熟悉端口号的使用情况,就类似文件描述符的分配,由操作系统进行管理
inet_addr
:核心工作将字符串分割的ip地址转化成四字节的网络序列IP。
#include
#include
#include
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
char *inet_ntoa(struct in_addr in):
把四个字节的IP地址转换成点分十进制IP,同时变成网络序列。
union{
unsigned int ip;
typedef struct {
unsigned int part1:8;
unsigned int part2:8;
unsigned int part3:8;
unsigned int part4:8;
}ip_seg;/*位段*/
}ip_t;
int main()
{
ip_t Ip;
Ip.ip =12345;
/*数字转换成字符串:分别获得四个部分,带上.进行转化成char*/
ip.ip_seg.part1,part2,part3,part4
/*字符串转化成数字:把四个字符分别转化成整数,直接读取ip值即可*/
}
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?
man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码
#include
#include
#include
using namespace std;
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
cout<<ptr1<<" "<<ptr2<<endl;
}
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
思考:
测试代码:
#include
#include
#include
#include
#include
#include
void* Func1(void* p) {
struct sockaddr_in* addr = (struct sockaddr_in*)p;
while (1) {
char* ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
sleep(1);
}
return NULL;
}
void* Func2(void* p) {
struct sockaddr_in* addr = (struct sockaddr_in*)p;
while (1) {
char* ptr = inet_ntoa(addr->sin_addr);
printf("addr2:%s\n",ptr);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid1 = 0;
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
recvfrom
:收数据,类似普通文件的read
。
flag
:表示等待的状态,可以阻塞(0)等或者非阻塞。
src_addr
:发送者,方便发回去。
addrlen
:发送的信息长度。
ssize_t
:-1表示错误,其他表示收到的数据字节大小。ssize_t(int),size_t(unsigned int)
socklen_t *addr
:既做输入又做输出,输入的时候表示传入的结构体大小,输出表示实际收到的结构体大小。
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
sendto
:发消息
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
实际真正在工程中写入的时候服务器不需要ip。也就是说在构造的时候只要传一个端口号。
网络地址为INADDR_ANY
,这个宏表示本地(server)的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
如果是IP地址是一个具体的IP地址,那么只有从这个IP地址上上来的报文才会交付。
云服务器可能因为安全问题没法在公网IP开启对应的端口号。
举个例子,平时去下馆子正常时间都能吃,是因为有人一直在等,这个等就是处于监听状态。
listen
将套接字状态设置为监听状态,允许任何时刻有客户端来连接。tcp不连接不能直接发送数据。udp可以。
#include
#include
int listen(int sockfd, int backlog);
backlog之后会再理解。
backlog
的理解先举个例子,比如有一家饭馆生意特别好,去吃的时候人已经满了,如果非要等那就等着,如果等着的人多了就要排队。因此backlog
表示底层连接来到之后但是该连接无法被立即处理时底层的连接队列的长度是多少。这叫全连接队列。还有半连接队列,和三次握手有关。
这个值一般不要设太大。
socket
:创建的套接字
address
:来连接的客户端的信息
后两个参数都是输出型参数,作用和udp的一样。
返回值int
是一个文件描述符。因此如果来10个连接请求,就会有11个连接请求。一个是之前申请的。
那么这里的这个返回值又和之前的有什么区别吗?
举个例子:美食小吃一条街上每家店门口都有一个拉客的,当把客人拉进来找到服务员招待客人之后,拉客的又接着去拉客人。拉客的和服务员都是一家店的。这里的sockfd
就是拉客(专注于从底层获取链接上来,是server的listen socket)的,而返回值就是用来对用户进行服务的(专注于进行通信)。张三会被很多拉客的人拉,如果张三拒绝了拉客的就会拉其他客人。
int accept(int sockfd, struct sockaddr* address,socklen_t* address_len);
帮助获取到对应的连接端的IP和端口号相关的信息。
一般的文件,pipe——stream
socket,tcp——stream
read,write,recv,send。
read,write不能用于udp。
更建议用recv
,比read
功能更多。
recv
:
flag
:默认为0,表示阻塞。
返回值
:正数表示读到的字节数,-1表示出错,0表示对端被关闭了。
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read
:
#include
ssize_t read(int fd, void *buf, size_t count);//count表示希望读到的字节,ssize_t表示实际收到的字节
更建议使用send
,比read
功能更多。
send
:
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
write
:
#include
ssize_t write(int fd, const void *buf, size_t count);
sockfd
:连接成功客户端通过创建的套接字(参数里的sockfd)向服务器发起通信。使用新socket是服务器的工作。
addr和addrlen
:和sendto接口一样,连接服务器,指明连接的具体进程。
返回值
:标识连接成功与否。如果为0则成功。
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
用于客户端
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
从上述的代码实例中可以看出,sockaddr
是统一的接口,只用一个接口完成不同套接字之间的通信问题。
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
设计成统一的目的是为了设计尽量少的接口。
共同点是三者的首地址都是存的地址类型。
达到同一套接口,传入参数的不同,进行不同的函数操作,达到了静态多态——函数重载。(该思想在讲述之前的“一切皆文件”的时候也提及了)
可以发现,所有的接口都是struct sockaddr
类型,如何区分对于具体的传入使用哪个结构体。提取第一个字段进行if
判断,如果为AF_INET
则为struct sockaddr_in
。(全大写的是宏)
udpServer.hpp
:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
class udpServer{
private:
std::string ip;
int port;
int sock;
public:
udpServer(std::string _ip="127.0.0.1",int _port = 8080)
:ip(_ip),
port(_port)
{}
void initServer()
{
sock = socket(AF_INET,SOCK_DGRAM,0); /*在系统资源上申请了一批网络通信需要的资源*/
std::cout<<sock<<std::endl;
/*把网络通信相关的数据和资源关联起来*/
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
{
std::cerr << "bind error" << std::endl;
exit(1);
}
}
/*echo server*/
void start()
{
char msg[64];/*udp面向数据块*/
for(; ;)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
0,(struct sockaddr*)&end_point,&len);
if( s > 0 )
{
msg[s]= '\0';
std::cout << "cliet# "<<msg<<std::endl;
std::string echo_string = msg;
echo_string += " [ severr echo! ]";
sendto(sock,echo_string.c_str(), echo_string.size(),0,\
(struct sockaddr*)&end_point , len
);
}
}
}
~udpServer()
{
close(sock);
}
};
udpClient.hpp
:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
class udpClient{
private:
std::string ip;
int port;
int sock;
public:
//ip,port? server's ip,port!!
udpClient(std::string _ip="127.0.0.1",int _port = 8080)
:ip(_ip),
port(_port)
{}
void initServer()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
std:: cout<<sock<<std::endl;
}
/*echo server*/
void start()
{
/*明确客户端发送报文给谁发*/
std::string msg;
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
for(; ;)
{
std::cout<<"Please Enter# ";
std::cin >>msg;
if(msg=="quit")
{
break;
}
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char echo[128];
ssize_t s = recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);/*可以nullptr也可以传具体的*/
if(s>0)
{
echo[s] = 0;
std::cout<<"server# "<<echo<<std::endl;
}
}
}
~udpClient()
{
close(sock);
}
};
sockaddr
获得(结构体中含有IP和port),存放所有的end_point
,当有人给server发消息时,给所有人转发。能拿到对应的IP就能对访问进行控制。比如说恶意攻击禁止IP。udpServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
class udpServer{
private:
// std::string ip;
int port;
int sock;
public:
udpServer(int _port = 8080)
:
port(_port)
{}
void initServer()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
std:: cout<<sock<<std::endl;
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
//local.sin_addr.s_addr = inet_addr(ip.c_str());
local.sin_addr.s_addr = INADDR_ANY;
if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
{
std::cerr << "bind error" << std::endl;
exit(1);
}
}
/*echo server*/
void start()
{
char msg[64];
for(; ;)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
0,(struct sockaddr*)&end_point,&len);
if( s > 0 )
{
/*也可以to_string*/
char buf[32];
sprintf(buf,"%d",ntohs(end_point.sin_port));
std::string cli = inet_ntoa(end_point.sin_addr);
cli +=":";
cli +=buf;
msg[s]= '\0';
std::cout<<cli <<"#"<<msg<<std::endl;
std::string echo_string = msg;
echo_string += " [ severr echo! ]";
sendto(sock,echo_string.c_str(), echo_string.size(),0,\
(struct sockaddr*)&end_point , len
);
}
}
}
~udpServer()
{
close(sock);
}
};
updServer.cc
#include"udpServer.hpp"
void Usage(std::string proc)
{
std::cout <<"Usage:"<<proc <<" local_port"<<std::endl;
}
//./udpServer ip port
int main(int argc,char *argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(1);
}
udpServer *up = new udpServer(atoi(argv[1]));
up->initServer();
up->start();
delete up;
}
这个过程其实就一个制定应用层协议的过程。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class udpServer{
private:
// std::string ip;
int port;
int sock;
std::map<std::string,std::string> dict;
public:
udpServer(int _port = 8080)
:
port(_port)
{
dict.insert(std::make_pair("apple","苹果"));
dict.insert(std::make_pair("banana","香蕉"));
dict.insert(std::make_pair("student","学生"));
}
void initServer()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
std:: cout<<sock<<std::endl;
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
//local.sin_addr.s_addr = inet_addr(ip.c_str());
//local.sin_addr.s_addr = INADDR_ANY;
local.sin_addr.s_addr = 0;
if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
{
std::cerr << "bind error" << std::endl;
exit(1);
}
}
/*echo server*/
void start()
{
char msg[64];
for(; ;)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
0,(struct sockaddr*)&end_point,&len);
if( s > 0 )
{
/*也可以to_string*/
char buf[32];
sprintf(buf,"%d",ntohs(end_point.sin_port));
std::string cli = inet_ntoa(end_point.sin_addr);
cli +=":";
cli +=buf;
msg[s]= '\0';
std::cout<<cli <<"#"<<msg<<std::endl;
std::string echo = "unknown";
auto ret = dict.find(msg);
if(ret != dict.end() )
{
echo = dict[msg];
}
//echo_string += " [ severr echo! ]";
sendto(sock,echo.c_str(), echo.size(),0,\
(struct sockaddr*)&end_point , len
);
}
}
}
~udpServer()
{
close(sock);
}
};
tcpServer.hpp
:
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5
#endif
class tcpServer{
private:
int port;
int listensock;//监听套接字
public:
tcpServer(int _port)
:port(_port),
listensock(-1)
{ }
void initServer()
{
/*创建套接字信息,文件相关信息*/
listensock = socket(AF_INET,SOCK_STREAM,0);
if(listensock < 0 )
{
std::cerr <<"listensocket error" <<std::endl;
exit(2);
}
/*用户层填充套接字信息*/
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
/*将文件信息和网络信息进行绑定*/
if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
std::cerr << "bind error" << std::endl;
exit(3);
}
/*将套接字设置为监听状态*/
if( listen(listensock ,BACKLOG ) <0 )
{
std::cerr <<"listen error" <<std::endl;
exit(4);
}
}
//BUG
void service(int sock)
{
char buffer[1024];
while(true)
{
//read or write ->ok
size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
if( s > 0 )
{
buffer[s] = 0;
std::cout<<"client# "<<buffer<<std::endl;
send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
}
}
}
void start()
{
sockaddr_in end_point;//远端
while(true)/*进程常驻内存*/
{
socklen_t len = sizeof(end_point);
int sock = accept(listensock,(struct sockaddr*)&end_point,&len);/*多个底层链接在应用层连接拿上来*/
if( sock < 0 )
{
std::cerr<<"accept error"<<std::endl;
continue;
}
/*通信*/
std::cout<< "get a new link..." <<std::endl;
service(sock); //进行IO服务
}
}
~tcpServer()
{ }
};
代码效果:
tcpClient.hpp
:
#ifndef __TCP_CLIENT_H
#define __TCP_CLIENT_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
class tcpClient{
private:
std::string svr_ip;
int svr_port;
int sock;
public:
tcpClient(std::string _ip="127.0.0.1" ,int _port = 8080)
:svr_ip(_ip),
svr_port(_port)
{}
void initClient()
{
sock = socket(AF_INET, SOCK_STREAM , 0);
if(sock < 0)
{
std::cerr<<"socket error"<<std::endl;
exit(2);
}
//bind?客户端不需要绑定,也不需要监听,因为没人连,也不需要accept,因为没有listen socket
struct sockaddr_in svr;
svr.sin_family = AF_INET;
svr.sin_port = htons(svr_port);
svr.sin_addr.s_addr = inet_addr( svr_ip.c_str() ) ;
if( connect(sock,(struct sockaddr*)&svr , sizeof(svr)) != 0 )
{
std::cerr<<"connect error" <<std::endl;
}
//connect success
}
void start()
{
char msg[64];
while(true)
{
size_t s = read(0,msg ,sizeof(msg)-1);
if( s>0 )
{
msg[s]=0;
send(sock,msg,strlen(msg),0);//文件发送不用'\0'
size_t ss = recv(sock,msg,sizeof(msg)-1,0);
if(ss > 0 )
{
msg[ss] = 0;
std::cout<<"server echo $" <<msg <<std::endl;
}
}
}
}
~tcpClient()
{
close(sock);
}
};
当关掉服务端之后客户端未关闭时服务端没法再启动
当客户端退出时:
在处理的时候通过recv\read
返回值为0时判断客户端是否退出了。可以发现写端退出读端关闭,tcp通信在细节上和管道特别像,两者都是流式,管道是以本地的文件作为通信双方的临界资源,现在临界资源是套接字或者网络。
简单回顾下前台进程转后台进程(&运行或者ctrl+z停止),jobs查看后台进程,以及bg1(让后台进程运行)或fg1将后台进程切换回前台。注意当进程中含有需要前台输入部分功能bg1没法唤醒停止的后台进程。(将client中的读取部分注释即可演示)
tcp.server.h
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5
#endif
class tcpServer{
private:
int port;
int listensock;//监听套接字
public:
tcpServer(int _port)
:port(_port),
listensock(-1)
{ }
void initServer()
{
listensock = socket(AF_INET,SOCK_STREAM,0);
if(listensock < 0 )
{
std::cerr <<"listensocket error" <<std::endl;
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
std::cerr << "bind error" << std::endl;
exit(3);
}
if( listen(listensock ,BACKLOG ) <0 )
{
std::cerr <<"listen error" <<std::endl;
exit(4);
}
}
void service(int sock)
{
char buffer[1024];
while(true)
{
//read or write ->ok
size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
if( s > 0 )
{
buffer[s] = 0;
std::cout<<"client# "<<buffer<<std::endl;
send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
}
else if( s== 0 )
{
std::cout <<"client close"<<std::endl;
close(sock);
break;
}
else{
std::cout<<"recv client data eroor.."<<std::endl;
close(sock);
break;
}
}
}
void start()
{
sockaddr_in end_point;//远端
while(true)
{
socklen_t len = sizeof(end_point);
int sock = accept(listensock,(struct sockaddr*)&end_point,&len);
if( sock < 0 )
{
std::cerr<<"accept error"<<std::endl;
continue;
}
/*通信*/
std::cout<< "get a new link..." <<std::endl;
service(sock);
}
}
~tcpServer()
{ }
};
tcpclient.hpp
#ifndef __TCP_CLIENT_H
#define __TCP_CLIENT_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
class tcpClient{
private:
std::string svr_ip;
int svr_port;
int sock;
public:
tcpClient(std::string _ip="127.0.0.1" ,int _port = 8080)
:svr_ip(_ip),
svr_port(_port)
{}
void initClient()
{
sock = socket(AF_INET, SOCK_STREAM , 0);
if(sock < 0)
{
std::cerr<<"socket error"<<std::endl;
exit(2);
}
//bind?客户端不需要绑定,也不需要监听,因为没人连,也不需要accept,因为没有listen socket
struct sockaddr_in svr;
svr.sin_family = AF_INET;
svr.sin_port = htons(svr_port);
svr.sin_addr.s_addr = inet_addr( svr_ip.c_str() ) ;
if( connect(sock,(struct sockaddr*)&svr , sizeof(svr)) != 0 )
{
std::cerr<<"connect error" <<std::endl;
}
//connect success
}
void start()
{
char msg[64];
while(true)
{
std::cout<<"Please Enter Message #";
std::endl;
size_t s = read(0,msg ,sizeof(msg)-1);
if( s>0 )
{
msg[s-1]=0; //去掉换行符,read不吸收
send(sock,msg,strlen(msg),0);//文件发送不用'\0'
size_t ss = recv(sock,msg,sizeof(msg)-1,0);
if(ss > 0 )
{
msg[ss] = 0;
std::cout<<"server echo $" <<msg <<std::endl;
}
}
}
}
~tcpClient()
{
close(sock);
}
};
当第二个客户端连接的时候显示连接不上?
因为原来的服务端是一个单执行流,当服务器等待客户端输入的时候没有得到输入导致服务器被挂起。单执行流会导致任何一个任务一旦发生阻塞问题,其他任务都不可能执行。
而对于等待子进程的问题要怎么处理?
signal(SIGCHLD,SIG_IGN);
另外还有一种小trick是子进程再创建进程,并且让子进程退出,父进程等待子进程可以立即完成,对于孙子进程变成孤儿进程被1号进程收养,不用再wait。(严重不推荐,fork()太多了,创建是有成本的)
同时注意父子进程关闭文件描述符的问题。父进程是一定要关闭的,不然会导致文件描述符越用越少。
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5
#endif
class tcpServer{
private:
int port;
int listensock;//监听套接字
public:
tcpServer(int _port)
:port(_port),
listensock(-1)
{ }
void initServer()
{
signal(SIGCHLD,SIG_IGN);
listensock = socket(AF_INET,SOCK_STREAM,0);
if(listensock < 0 )
{
std::cerr <<"listensocket error" <<std::endl;
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
std::cerr << "bind error" << std::endl;
exit(3);
}
if( listen(listensock ,BACKLOG ) <0 )
{
std::cerr <<"listen error" <<std::endl;
exit(4);
}
}
void service(int sock)
{
char buffer[1024];
while(true)
{
//read or write ->ok
size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
if( s > 0 )
{
buffer[s] = 0;
std::cout<<"client# "<<buffer<<std::endl;
send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
}
else if( s== 0 )
{
std::cout <<"client close"<<std::endl;
close(sock);
break;
}
else{
std::cout<<"recv client data eroor.."<<std::endl;
close(sock);
break;
}
}
}
void start()
{
sockaddr_in end_point;//远端
while(true)
{
socklen_t len = sizeof(end_point);
int sock = accept(listensock,(struct sockaddr*)&end_point,&len);
if( sock < 0 )
{
std::cerr<<"accept error"<<std::endl;
continue;
}
std::string cli_info = inet_ntoa(end_point.sin_addr);
cli_info += ":";
cli_info += std::to_string(ntohs(end_point.sin_port));
/*通信*/
std::cout<< "get a new link..." <<cli_info<<"sock: "<<sock<<std::endl;
pid_t id = fork();
if( id == 0 )
{
//子进程关心的是用于IO的sock,因此推荐关闭listensock,子进程是自己的拷贝部分,不影响父进程
close(listensock);
service(sock);
exit(0);
}
//父进程关心用于接客户端的listensock,关闭sock;父进程和子进程是不同的文件描述表
close(sock);//父进程必须关闭,不断获取新连接导致文件描述符越用越多被子进程继承
}
}
~tcpServer()
{ }
};
每个子进程用的文件描述符都是4。
在单独服务器,其他主机群的条件下发现,当有客户端连接时网络状态可以检测到tcpServer的增多。因此如果其中两个tcpServer(两个客户端)之间要互相发消息,就是再进行一个进程间通信的逻辑。同时来了一个客户端就创建一个进程,一对客户的时间成本增加,如果客户端达到小型规模数量进程过多服务器就跑不动了。
此时进行ps ajx| head -1 && ps ajx | grep -ER
查看发现只有一个进程,使用之前学习的ps -aL
查看轻量级进程。
多线程之间共享文件描述符所以不能关闭。
同时注意对新获得的sock要在堆上备份。
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5
#endif
class tcpServer{
private:
int port;
int listensock;//监听套接字
public:
tcpServer(int _port)
:port(_port),
listensock(-1)
{ }
void initServer()
{
signal(SIGCHLD,SIG_IGN);
listensock = socket(AF_INET,SOCK_STREAM,0);
if(listensock < 0 )
{
std::cerr <<"listensocket error" <<std::endl;
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
std::cerr << "bind error" << std::endl;
exit(3);
}
if( listen(listensock ,BACKLOG ) <0 )
{
std::cerr <<"listen error" <<std::endl;
exit(4);
}
}
static void service(int sock)
{
char buffer[1024];
while(true)
{
//read or write ->ok
size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
if( s > 0 )
{
buffer[s] = 0;
std::cout<<"client# "<<buffer<<std::endl;
send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
}
else if( s== 0 )
{
std::cout <<"client close"<<std::endl;
close(sock);
break;
}
else{
std::cout<<"recv client data eroor.."<<std::endl;
close(sock);
break;
}
}
}
static void *serviceRoutine(void *arg)
{
pthread_detach(pthread_self());//主线程不等待就可能内存泄漏,而等待又被阻塞,因此进行线程分离
std::cout<<" create a new thread for IO"<<std::endl;
int *p = (int*)arg;
int sock = *arg;
service(sock); //service没有用到类内成员函数,因此可以改成static
delete p;
}
void start()
{
sockaddr_in end_point;//远端
while(true)
{
socklen_t len = sizeof(end_point);
int sock = accept(listensock,(struct sockaddr*)&end_point,&len);
if( sock < 0 )
{
std::cerr<<"accept error"<<std::endl;
continue;
}
std::string cli_info = inet_ntoa(end_point.sin_addr);
cli_info += ":";
cli_info += std::to_string(ntohs(end_point.sin_port));
/*通信*/
std::cout<< "get a new link..." <<cli_info<<"sock: "<<sock<<std::endl;
pthread_t tid;
int * p =new int(sock);
pthread_create(&tid, nullptr,serviceRoutine,(void*)p);//bug:新线程没有创建完主线程往下继续执行拿了新的sock,导致sock变了。因此要保存一下之前的变量
}
}
~tcpServer()
{ }
};
实现短链接,客户端处理一次业务收到就退出。
查看代码实现
大量客户端:系统会存在大量的执行流,执行流之间的切换周期变长,切换有可能成为效率低下的重要原因。
第一种不可使用,后两者只能适用小型应用,比如局域网。
不管多进程多线程,目前都是客户来了再服务,效率低下。
netstat -nltp
:n-num,l-list/listen,t-tcp,p-process
netstat -nlup
:n-num,l-list/listen,t-tcp,p-process
一个服务器在一段时间内会收到很多客户端链接,一旦链接了服务器,服务器就需要对外提供多种链接。系统中当链接足够多的时候操作系统就要先描述再组织描述链接。
**所谓的面向连接本质在创建连接成功之后双方都要为了维护该连接在系统层面创建对应的数据结构来保存对应的连接数据。**双方为了维护连接是有成本的,成本体现在创建数据结构及变量要花时间和空间。Udp不用连接,会快一点,吃资源少一点。
一般而言,服务器被动接收连接,客户端主动发起请求,像这种客户端主动发起请求,服务器被动接收连接的模型叫CS模型。
而在TCP中,客户端建立连接的方式叫三次握手。断开连接的方式叫四次挥手。
主动建立连接的地方肯定是客户端,但是断开连接可能是客户端也可能是服务器端。这里以客户端主动断开连接为例。
三次握手和四次挥手的详细细节后续再说。
三次握手的过程中创建对应的数据结构,四次挥手把曾经申请的资源释放掉。建立和释放连接要花费系统资源,和客户端还是服务器没关系。服务端和客户端在技术层面上是平等的。
具体的细节之后再学习。下面会具体讲socket()
和fd
的关系。
connect
:客户端向服务器发送一个携带SYN
的数据报,其中connect
是一次函数调用,为系统调用接口,通信双方主机在客户端发送connect时底层自动进行三次握手,因此connect是触发链接建立的作用。
accept
:底层把链接建立好后accpet
才会返回。
write
:等同于send
。
发送DATA之后的ACK
:保证可靠性,并且双方通信地位对等。
close
:结束端向另一端发送FIN
,另一端自动ACK
。一个close
对应两个动作,发FIN和收ACK。
三次握手的例子:
小明:做我女朋友
小红:好,什么时候开始这段感情
小明:就现在
至此两者建立链接。
四次挥手的例子:
小红:我要跟你离婚
此时只有单方面的离婚。断开连接是双方的事情。
小明:好啊
小明:我也要和你离婚
小红:好啊
四次挥手本质是双方都要认可。
切片和多态的概念:socket底层也采用了这种设计方式。
#include
struct Stu{
char name[100];
char sex;
};
struct good_Stu{
struct Stu base;
int good1;//可以直接通过强转指针类型访问
};
struct A{
int x;
};
struct B{
struct A obj;
int y;
};
struct C{
struct B obj;
int z;
};
int main(){
struct C c;
struct C *p =&c;
struct B *p1 = (struct B*)&c;
struct A *p2 = (struct A*)&c;
}
总结一下socket和文件的映射关系:
一个进程创建套接字的时候本质先创建一个struct file
得到文件描述符,同时创建struct socket
,让两者关联,struct file
的void* private_data
指向struct socket
,struct socket
的*file
回指向struct file
。
struct sock
中的sock* sk
会指向struct tcp_sock
,tcp_sock
里面是一摞的结构体,就如上面的demo一样。这部分暂时不细谈,按下不表。
现在也就理解了创建一个套接字本质上就是打开了一个文件,创建一个文件就是通过void* private_data指向套接字socket。
通过socket的read/write干了什么事情?
read参数传入文件描述符,通过文件描述符找到struct file
,再由void *private_data
找到socket,通过socket找到tcp_socket/sock,里面有receive_queue
,拿走数据。
write同理。