每台因特网主机都运行实现TCP/IP协议(Transmission ControlProtocol/Internet Protocol,传输控制协议/互联网络协议)的软件,几乎每个现代计算机系统都支持这个协议。因特网的客户端和服务器混合使用套接字接口函数和UnixIO函数来进行通信。套接字函数典型地是作为会陷入内核的系统调用来实现的,并调用各种内核模式的TCP/IP函数。
从程序员的角度,我们可以把因特网(Internet)看做一个世界范围的主机集合,满足以下特性:
-主机集合被映射为一组32位的IP地址。
-这组IP地址被映射为一组称为因特网域名(Internet domain name)的标识符。
-因特网主机上的进程能够通过连接( connection )和任何其他因特网主机上的进程通信。
TCP/IP协议简单理解
TCP/IP实际上是一个协议族,其中每一个都提供不同的功能。
- IP协议提供基本的命名方法和递送机制,这种递送机制能够从一台因特网主机往其他主机发送包,也叫做数据报(datagram)。IP 机制从某种意义上而言是不可靠的,因为,如果数据报在网络中丟失或者重复,它并不会试图恢复。
- UDP( Unreliable Datagram Protocol,不可靠数据报协议)稍微扩展了IP协议,这样一来, 包可以在进程间而不是在主机间传送。
- TCP是一个构建在IP之上的复杂协议,提供了进程间可靠的全双工(双向的)连接。
简单总结:网络层IP协议可以在主机之间递送数据,而传输层TCP/UDP协议有了端口的概念后,就可以完成进程间的数据传送了。
再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个连接(可以通过netstat -n查看)。
netstat是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态
常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
端口号范围划分
- 0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的.
- 1024 - 65535: 操作系统动态分配的端口号,客户端程序的端口号, 就是由操作系统从这个范围分配的.
常见知名端口号
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
Web服务器的知名名字为http, 使用知名端口80
ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
https服务器, 使用443
执行下面的命令, 可以看到知名端口号和知名名字的映射
cat /etc/services
注意:
1.一个进程可以绑定(bind)多个端口,但是一个端口号不能绑定(bind)多个进程。
2.我们自己写一个程序使用端口号时, 要避开这些知名端口号.
连接
因特网客户端和服务器通过在连接上发送和接收字节流来通信。从连接一对进程的意义 上而言,连接是点对点的。从数据可以同时双向流动的角度来说,它是全双工的。并且从由源进程发出的字节流最终被目的进程以它发出的顺序收到它的角度来说,它也是可靠的。一个套接字是连接的一 个端点。每个套接字都有相应的套接宇地址,是由一个因特网地址(ip)和一个16位的整数端口(port)组成的,用“地址:端口”来表示。
当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。然而,服务器套接字地址中的端口通常是某个知名的端口,是和这个服务相对应的
。
一个连接是由它两端的套接字地址唯一确定的。 这对套接字地址叫做套接字对。
例如:
UDP协议简单介绍
1.UDP的特点(UDP传输的过程类似于寄信)
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量,是整条整条的发送的;即应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
例如:用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 一次接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节。
2.UDP的缓冲区
UDP没有真正意义上的发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 且如果缓冲区满了, 再到达的UDP数据就会被丢弃。
UDP的socket既能读, 也能写, 是全双工的。
UDP套接字的编程流程及接口介绍
服务端( server) :
1.创建套接字:将进程和网卡建立联系
2.绑定地址信息:将端C和进程联系起来;绑定IP地址+ Port
3.接收数据
4发送数据
5.关闭套接字
客户端( client):
1.创建套接字
2.綁定地址信息
3.发送数据
4.接收数据
5.关闭套接字
注意:因为操作系统会自动帮我们绑定地址信息,所以客户端第二步可以不用绑定。要是自己绑定地址信息,也就是意味着固定客户端的端口了,由于一个端口号不能同时被多个进程所占用。当前机器就只能启动一个客户端程序了。
1.创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器通用)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
domain:地址域,传入协议的版本
网络层: AF_INT-->ipv4版本的ip协议
AF_INET6-->ipv6版本的ip协议
type:套接字的类型是啥
运输层: tcp : SOCK_STREAM:流式套接字;默认的协议就是tcp ,不支持udp的
udp: SOCK_DGRAM:数据报套接字;默认读的协议就是udp ,不支持tcp的
protocol:协议
0 :采用套接字默认协议
tcp: IPPROTO_TCP(6)
udp:IPPROTO_UDP(17)
返回值:
成功返回套接字的操作句柄,其实就是一个文件描述符;一般称之为 套接字描述符
失败返回-1
2.绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *addr,socklen_t addrlen);
sockfd:套接字操作句柄
addr: 通用套接字地址结构,为了很好的兼容不同协议的地址信息;
addrlen:地址信息长度
注意:socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6, 然而, 各种网络协议的地址格式并不相同,所以bind接口(和connect、accept接口)在设计的时候为了通用各种协议。定义了一个通用套接字结构体struct sockaddr ;而在进行具体协议地址信息绑定的时候,填充不同的结构体,之后将结构体的对象的地址,强转传参给bind函数。所以虽然socket API的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in。
因为IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
socklen:地址信息的长度,防止有的协议的地址信息长度大于16个字节。
// 通用套接字地址结构(用于连接、绑定和接受)
struct sockaddr {
unsigned short sa_ family; //地址域
char sa_ data[14] ; 地址信息
};
// ipv4套接字地址
struct sockaddr_in {
unsigned short sin_ family; //地址域(簇),总是AF_INET
unsigned short sin_ port; //按网络字节顺序排列的端口号
struct in_ addr sin_ addr ; //按网络字节顺序排列的IP地址
unsigned char sin_ zero[8]; //8字节的填充字节
};
//ip地址结构
typedef uint32_t in_addr_t; //无符号32位整型
struct in_addr { {
in_addr_t s_addr;
};
强转类型时的内部具体操作
3.发送数据
ssize_t sendto(int sockfd,const void* buf,size_t len,int falgs,const struct sockaddr *dest_addr,socklen_t addrlen);
sockfd:套接字描述符
buf:待发送的数据
len:发送的数据长度
falgs:
0:阻塞发送
dest_addr:目标主机地址信息
addrlen:地址信息长度
//只需要知道目的主机地址信息里的ip和端口号就可以发送消息
1.UDP也是有发送缓冲区的,只不过UDP的特点是整条发送,所以在发送缓冲区当中打上UDP协议报头之后就提交给网络层。
2.阻塞发送的含义是,当发送缓冲区当中有数据的时候,再次调用sendto接口,会阻塞等待,直到上一条数据发送完毕, sendto才将数据写到发送缓冲区当中。
4.接收接口
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_ t* addrlen);
sockfd:套接字描述符
buf:从接收缓冲区当中拿到的数据存到一个buffer当中
len:buffer的最大长度,意味着最大可以接收多少数据,预留"\0” 的位置
flags:
0 :阻塞接收
src_addr:源主机的地址信息(标识这条数据从哪一个主机上面哪一个进程来)
addrlen:地址信息长度,同时要注意这里是一个输入输出型参数
//这样就知道是谁发的信息给我,我就好利用这些参数里保存的信息来找到目标再向它发送消息了
5.关闭套接字的接口
close(int sockfd);
基于udp的客户端-服务器端的的通信小程序
封装客户端和服务端的通信接口
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
class Udpcs
{
public:
Udpcs()
{
sock_=-1;
}
~Udpcs()
{
}
//创建套接字
bool CreateSocket()
{
sock_=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(sock_<0)
{
perror("socket");
return false;
}
return true;
}
//绑定地址信息 服务器端调用。客户端由内核自动分配
bool Bind(string& ip,uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family =AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());//将点分十进制的ip转换成二进制的网络字节序
int ret=bind(sock_,(struct sockaddr*)&addr,sizeof(addr));
if(ret<0)
{
perror("bind");
return false;
}
return true;
}
//3.发送数据
//udp只需要知道对方的ip及端口号,就可以不用连接就发送消息
//dest_addr 目的地址
bool Send(string& data,struct sockaddr_in* dest_addr)//结构体里保存的ip和端口号等
{
int sendsize=sendto(sock_,data.c_str(),data.size(),0,(struct sockaddr*)dest_addr,sizeof(struct sockaddr_in));
if(sendsize<0)
{
perror("send");
return false;
}
return true;
}
//4.接收数据
//src_addr 原地址
bool Recv(string* buf,struct sockaddr_in* src_addr)
{
char tmp[1024];//临时接收和保存数据
memset(tmp,'\0',sizeof(tmp));
socklen_t socklen=sizeof(struct sockaddr_in);
int recvsize=recvfrom(sock_,tmp,sizeof(tmp)-1,0,(struct sockaddr*)src_addr,&socklen);
if(recvsize<0)
{
perror("recvform");
return false;
}
(*buf).assign(tmp,recvsize);
return true;
}
// 5.关闭套接字
void Close()
{
close(sock_);
sock_=-1;
}
private:
int sock_;//定义描述符
};
客户端逻辑
#include"udpclass.hpp"
//对于客户端而言,命令行参数中的ip和端口号需要指定为服务端的
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("arg num is error");
return 0;
}
string ip(argv[1]);
uint16_t port=atoi(argv[2]);
Udpcs uc;
if(!uc.CreateSocket())
{
return 0;
}
//把传进来的服务器的地址信息组织起来,以便sendto函数使用
struct sockaddr_in dest_addr;//客户端的目标地址就是服务端的地址信息
dest_addr.sin_family=AF_INET;
dest_addr.sin_port=htons(port);
dest_addr.sin_addr.s_addr=inet_addr(ip.c_str());
while(1)
{
//发送数据
printf("client say:");
fflush(stdout);
string buf;
cin>>buf;
uc.Send(buf,&dest_addr);
//接收数据
struct sockaddr_in peeraddr;
uc.Recv(&buf,&peeraddr);
printf("server reply: %s\n",buf.c_str());
}
uc.Close();
return 0;
}
服务端逻辑
#include"udpclass.hpp"
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("arg num is error!\n");
return 0;
}
//从参数列表里传入的ip和端口号
string ip=argv[1];
uint16_t port=atoi(argv[2]);
Udpcs us;
if(!us.CreateSocket() )
{
return 0;
}
if(!us.Bind(ip,port))
{
return 0;
}
//绑定好了地址信息,准备接收和回复消息
while(1)
{
//接收数据
string buf;
struct sockaddr_in peeraddr;//对端的地址信息
us.Recv(&buf,&peeraddr);//都是出参,会保留对端的地址信息,之后回复会用
printf("client say:%s\n",buf.c_str());
//接着回复数据
printf("server reply:");
fflush(stdout);
cin>>buf;
us.Send(buf,&peeraddr);
}
//对话结束后,关闭描述符
us.Close();
return 0;
}