网络套接字(Udp与Tcp应用)
Posted 楠c
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络套接字(Udp与Tcp应用)相关的知识,希望对你有一定的参考价值。
目录
在网络通信中,凡是我们所写的代码,采用的接口都是系统调用接口,编写的程序都叫用户层程序,我们接下来的工作就是在用户层自定义协议。在网络模型中就是应用层,那么就是要使用传输层的接口(但是有原始套接字可以绕过传输层)
1. 认识套接字
1.1 IP
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
有了IP地址能够把消息发送到对方的机器上,但是跨网络传输还需要有一个其他的标识来区分出, 这个数据要给对方的哪个程序进行解析。
即IP在公网当中全网标识一台主机,发送的时候,不仅需要目的IP,通信也要自己的源IP也发过去,因为对方主机还要对你做出“回应”。
在你打开网页,访问百度的时候,实际上硬件只是一个载体,实际上通信的是你笔记本上的软件(浏览器进程),和对方服务器上的软件(服务器进程)。更准确的一点说,实际上是运行起来的进程进行通信,所以套接字的本质就是跨网络的进程间通信。
1.2 端口号
一个笔记本上,有很多进程,所有的进程并发的进行运行。所以通信的时候还需要一个东西来标识某个进程,标定进程的方式叫端口号。
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;就像你到了那台主机之还要根据目的端口号找到对应进程
1.3 套接字
IP标记某个全网唯一主机。
端口号标识主机内为一进程。那么IP+端口号就实现了标识全网内的唯一进程。而这个IP+端口号就是套接字。
服务器几乎永远不会关机,只会不断更新。虽然这里只简单的花了两个进程。但是这两台主机中充斥着大量的进程。公网IP保证了主机的唯一性,端口号保证了进程的唯一性。进程间通信,不同的进程看到了同一套资源,而跨网络进程通信,不同主机的进程就看到的是网络这个资源。
网络之中充斥着大量的套接字就要被管理起来
这里面有一个熟悉的file而file中存在一个
又指向这个socket指针
ops指针中存在着各种函数指针
1.4 端口号和进程id
pid表示唯一一个进程,并不是所有的进程都需要端口号,但是所有的进程在系统层面上都有一个pid。只有你这个进程是网络进程时才需要端口号。
一个进程可以绑定多个端口号
一个端口号只能用于一个进程
最开始收到数据的一定是计算机当中的网卡,然后自底向上交付。
1.5 认识TCP协议
- 传输层协议
- 有连接
- 可靠传输(要保证处理数据,丢包,等等,比较复杂)
- 面向字节流
管道也是面向字节流的。
1.6 认识UDP协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.7 网络字节序
低字节位在低地址处,叫做小端。
高字节位在低地址处,叫做大端。
假如你发数据,对方服务器可不知道你的数据你发的数据是大端还是小端,假如你的笔记本是小端,对方服务器是大端,那么服务器就会数据理解错误,这种情况肯定是存在,而且要被解决的。
网络规定,网络上跑的数据默认大端。假如你是小端机操作系统就会默认转为大端,收方默认接受的就是大端数据。
假设要发送0x1234abcd
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
大端发送很方便,由于首先收到的就是高权值数据,所以就可以边计算便接收
1.8 库函数
大端就不做任何转换,小端机调用这些函数,将数据转换成大端序列。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址(点分十进制,四个.
,一个点隔开一个字节,每个范围为0-255)转换后准备发送
1.9 地址转换函数
这个inet_ntoa函数返回一个char*,实际把它存储在静态存储区,也就是说,作为一个静态局部变量,虽然他的作用域依旧在函数内,但是生命周期却变成了整个文件。也就是说多线程,会有线程安全问题。新的一次会把老的一次覆盖掉。虽然在当前环境测试没有出现问题,可能是新版本添加了互斥锁。但是不推荐使用这个函数,可以用inet_ntop来代替。
1.10 socket 常见API
- 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
domain代表域,即使用TCP或者UDP
套接字类别,流式套接字,数据报套接字,原始套接字
协议,操作系统使用默认行为
最重要的是返回值
可以这么理解,网卡也是一种文件,通信之前需要将文件打开,这里的socket函数等价于open,返回值等价一个文件描述符。
- 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
关联IP,端口号。服务器一般不发消息,永远是被动的,即绑定的,IP,端口号是客户端自己的。在系统方面表明,将IP信息与网络信息关联起来。
- 描述符
- 将IP地址,端口号,填进去强转之后的结构体,然后绑定。
- 结构体长度
- 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
- 文件描述符
- 由于是面向连接的,所以需要一个等待队列,backlog代表底层连接的长度,一般不要设置太大
- 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
- 参数socket用于接收到链接请求(唯一一个),返回值socket用于通信(多个)
- 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
1.11 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同,操作系统实现了一种套接字接口,来解决不同套接字的编写,调用。
他就是sockaddr结构,就是用于将我们的IP,端口号等填入结构体,发给别人
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,
- 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API中的函数形参,可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是多态,即程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。那么强转后,在函数内怎么区分呢?由于每个字段都是16位,把他们提取出来,用if判断类型。就好了
2. Udp服务器
2.1 收发接口
服务器被动的收发数据,所需要接口。
收数据
- 文件描述符
- buf缓冲区
- 期望读的长度
- flags,读取条件是否成立
- 输出型参数,传入方信息
- 既做输入又做输出,输入代表结构体大小,输出代表读取结构体大小
发数据
- 文件描述符
- 发送缓冲区
- 期望发的长度
- 读取条件是否成立
- 刚才收数据收到了对方的信息
- 输入代表结构体大小,输出代表读取大小
2.2 udp服务器实现
实际上这里的ip,可以不用输入,直接在填充的时候选择INADDR_ANY,这样在客户端输入任意IP,输入端口号,都可以访问服务器。
#pragma once
#include<iostream>
#include<stdlib.h>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
class udpServer
{
private:
string ip;
int port;
int sock;
public:
udpServer(string _ip="127.0.0.1",int _port=8080)
:ip(_ip)
,port(_port)
{}
void initServer()
{
//创建socket描述符,默认为3
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
//填充信息到sockaddr _in中
struct sockaddr_in local;
local.sin_family=AF_INET;
//转成大端
local.sin_port=htons(port);
//sockaddr中有一个sin_addr结构体,结构体中的saddr为ip
//将ip转为char*
local.sin_addr.s_addr=inet_addr(ip.c_str());
//绑定端口号
//可以让不同类型套接字,使用同一套接口,所以要强转
//为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
cerr<<"bind error!\\n"<<endl;
exit(1);
}
}
void strat()
{
char msg[64]={0};
for(;;)
{
//远端的信息
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';
cout<<"client##"<<msg<<endl;
string echo_string=msg;
echo_string+="[注:服务器回显]";
sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
}
}
}
~udpServer()
{
close(sock);
}
};
main函数只需要简单的启动即可
#include"udpServer.hpp"
int main()
{
udpServer *up=new udpServer;
up->initServer();
up->strat();
delete up;
return 0;
}
由于此时还没有客户端,那么怎么看到他运行起来了呢
用netstat -nlup命令,其中u代表udp,假如是 -bltp就是tcp。
服务端已启动。
3. udp客户端实现
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
class udpClient
{
private:
string ip;
int port;
int sock;
public:
//连接服务器,服务器ip,port。
udpClient(string _ip="127.0.0.1",int _port=8080)
:ip(_ip)
,port(_port)
{}
void initClient()
{
//创建socket描述符,默认为3
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
//客户端不需要绑定
//填充信息到sockaddr _in中
//struct sockaddr_in local;
//local.sin_family=AF_INET;
//转成大端
//local.sin_port=htons(port);
//sockaddr中有一个sin_addr结构体,结构体中的saddr为ip
//将ip转为char*
//local.sin_addr.s_addr=inet_addr(ip.c_str());
//绑定端口号
//可以让不同类型套接字,使用同一套接口,所以要强转
//为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。
// if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
// {
// cerr<<"bind error!\\n"<<endl;
// _exit(1);
//}
}
void strat()
{
string msg;
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(port);
//点分十进制转成4字节,主机序列转成网络序列
peer.sin_addr.s_addr=inet_addr(ip.c_str());
for(;;)
{
cout<<"请输入"<<endl;
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);
if(s>0)
{
echo[s]='\\0';
cout<<"server###"<<echo<<endl;
}
}
}
~udpClient()
{
close(sock);
}
};
main中初始化,然后启动
#include"udpClient.hpp"
int main()
{
udpClient uc;
uc.initClient();
uc.strat();
return 0;
}
这里提到客户端不需要自己绑定,但是为什么服务器就需要绑定呢?
服务器:
-
一般服务器端口是总所周知的,ip和port不需要也不能轻易的更改。比如:http对应的端口号是80 https:443 ssh:22
-
服务器面对的客户很多,服务器一旦改了,客户端立马找不到,就无法访问服务器了。
客户端:
-
客户有很多客户端,如果绑定,就需要规定什么软件用什么端口,端口是标识进程的,一个端口只能对应一个进程,如果多个进程使用同一个端口,就会导致绑定是失败。并且这种让不同的公司进行沟通进行约定,是很不现实的。如果进行了bind会发生端口冲突,导致客户端无法启动
-
客户端需要唯一性,但不需要明确告诉你是哪个端口,因为也没有人去连接你,但是必需要IP和port。客户端使用udp服务器进行数据的交互之时,系统会自动进行Ip和端口号的绑定。
3.1 实验现象
客户端发数据,服务端收到,并回显到客户端。
而我们也可以,在main函数中传入参数,在命令行中带入ip与port,输入失败的时候,提示帮助手册。
客户端传入服务器的ip,port
3.2 本地环回
127.0.0.1,通常用来进行网络通信代码的本地测试,一般把网络层全部自顶向下,自底向上,跑一遍。进行测试。
4. 单进程Tcp
4.1 服务器
#ifndef __TCP__SERVER_H_
#define __TCP__SERVER_H_
#include<iostream>
#include<string>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
using namespace std;
class tcpServer
{
private:
int port;
int l_sock;
public:
tcpServer(int _port)
:port(_port)
,l_sock(-1)
{}
void initServer()
{
l_sock=socket(AF_INET,SOCK_STREAM,0);
if(l_sock<0)
{
cerr<<"socket error"<<endl;
exit(2);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(l_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
cerr<<"bind error"<<endl;
exit(3);
}
if(listen(l_sock,5)<0)
{
cerr<<"bind error"<<endl;
exit(4);
}
}
void service(int sock)
{
while(true)
{
//读取甚至可以用read
//udp用recvfrom,tcp用recv
//写可以用write
//udp用sendto,tcp用send
char buffer[24]={0};
size_t s=recv(sock,buffer,sizeof(buffer)-1,0);
if(s>0)
{
buffer[s]={0};
cout<<"client#: "<<buffer<<endl;
send(sock,buffer,strlen(buffer),0);
}
//不写这句他就会阻塞在send或recv,写上,当s==0时就退出
else if(s==0)
{
cout<<"client quit"<<endl;
close(sock);
break;
}
else{
cout<<"recv client data error"<<endl;
break;
}
}
close(sock);
}
void start()
{
sockaddr_in endpoint;
while(true)
{
//重新获取一个socket,加上原来的此时共有两个
socklen_t len=sizeof(endpoint);
int sock=accept(l_sock,(struct sockaddr*)&endpoint,&len);
if(sock<0)
{
cerr<<"accept error"<<endl;
continue;
}
cout<<"get a new link"<<endl;
//当客户端退出,service也应该退出
service(sock);
}
}
~tcpServer()
{
close(sock);
}
};
#endif
全0表示任意IP都可以。
虽然没有客户端,但是远程登录工具可以登录服务器
也可以进行通信
4.2 客户端
#include<iostream>
#include<string>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class tcpClient{
private:
int svr_port;
string svr_ip;
int sock;
public:
tcpClient(string _ip="127.0.0.1",int port=8080)
:svr_port(port)
,svr_ip(_ip)
{}
void initClient()
{
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
cerr<<"sock error"<<endl;
exit(2)以上是关于网络套接字(Udp与Tcp应用)的主要内容,如果未能解决你的问题,请参考以下文章
网络LinuxLinux网络编程-TCP,UDP套接字编程及代码示范
网络LinuxLinux网络编程-TCP,UDP套接字编程及代码示范