目录
一、背景知识
1.1 端口号
1.2 网络字节序
1.3 地址转换函数
二、Socket简介
三、套接字相关的函数
3.1 socket()
3.2 bind()
3.3 connect()
3.4 listen()
3.5 accept()
3.6 read()/recv()/recvfrom()
3.7 send()/sendto()
3.8 close()
四、UPD客服/服务端实验
一、背景知识
1.1 端口号
端口号是访问服务器的标识,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。
端口包括逻辑端口和物理端口两种类型:
物理端口是用于连接物理设备之间的接口,如ADSL Modem、集线器、交换机、路由器上用于连接其他网络设备的接口,如RJ-45端口、SC端口等等 。
逻辑端口是指逻辑意义上用于区分服务的端口,比如用于浏览网页服务的80端口,用于FTP服务的21端口等。如TCP/IP协议中的服务端口,通过不同的逻辑端口来区分不同的服务。
注意事项:
- 一个IP地址的端口通过16bit进行编号,最多可以有65536个端口 。
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- IP地址+端口号能够标识网络上的某一台主机的某一进程。
- 一个进程可以绑定多个端口号,一个端口号只能被一个进程占用。
1.2 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据,如果当前发送主机是小端, 就需要先将数据转成大端,否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint32_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint32_t ntohs(uint16_t netshort);
- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1.3 地址转换函数
使用bind()进行ip地址和端口绑定时,要创建sockaddr_in类型变量,把端口号和ip地址都填输入进sockaddr_in类型变量中,sockaddr_in中的成员变量sin_addr用于填入ip地址,其类型为struct in_addr,表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP 地址(例如"127.0.0.1"),但我们在sockaddr_in中需要填入in_addr类型表示的IP地址,以下函数可以在字符串表示和in_addr类型表示之间转换;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//字符串表示转in_addr类型表示函数
int inet_aton(const char *cp, struct in_addr *inp);
//in_addr只有一个成员变量s_addr其类型为in_addr_t
in_addr_t inet_addr(const char *cp);
// in_addr类型表示转字符串表示函数
char *inet_ntoa(struct in_addr in);
二、Socket简介
套接字(socket)是一种通信机制,凭借这种机制,客户端<->服务器 模型的通信方式既可以在本地设备上进行,也可以跨网络进行。套接字的创建和使用与管道是有区别的,因为套接字明确地将客户端、服务器区分开来,而且套接字机制可以实现将多个客户连接到一个服务器。
在Socket中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作,类似的,在网络中,我们也可以对Socket套接字进行这样的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。它跟我们的文件描述符非常像,其实就是一个整数,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。当应用程序要为网络通信而创建一个套接字(socket)时,操作系统就返回一个整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用Socket API接口的函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。
三、套接字相关的函数
套接字相关函数需要包含的头文件如下:
#include <sys/types.h>
#include <sys/socket.h>
3.1 socket()
int socket(int domain, int type, int protocol);
socket()函数用于创建一个socket描述符(socket descriptor),它唯一标识一个socket,这个socket描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
- domain:参数domain表示该套接字使用的协议族,在Linux系统中支持多种协议族,对于TCP/IP协议来说,选择AF_INET就足以,当然如果你的IP协议的版本支持IPv6,那么可以选择AF_INET6,可选的协议族具体见:
AF_UNIX, AF_LOCAL: 本地通信
AF_INET : IPv4
AF_INET6 : IPv6
AF_IPX : IPX - Novell 协议
AF_NETLINK : 内核用户界面设备
AF_X25 : ITU-T X.25 / ISO-8208 协议
AF_AX25 : 业余无线电 AX.25 协议
AF_ATMPVC : 访问原始ATM PVC
AF_APPLETALK : AppleTalk
AF_PACKET : 底层数据包接口
AF_ALG : 内核加密API的AF_ALG接口
- type:参数type指定了套接字使用的服务类型,可能的类型有以下几种:
SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的Socket服务,多用于资料(如文件)传输,如TCP协议。
SOCK_DGRAM:是提供无保障的面向消息的Socket 服务,主要用于在网络上发广播信息,如UDP协议,提供无连接不可靠的数据报交付服务。
SOCK_SEQPACKET:为固定最大长度的数据报提供有序的,可靠的,基于双向连接的数据传输路径。
SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。
SOCK_RDM:提供不保证排序的可靠数据报层。
- protocol:参数protocol指定了套接字使用的协议,在IPv4中,只有TCP协议提供SOCK_STREAM这种可靠的服务,只有UDP协议提供SOCK_DGRAM服务,对于这两种协议,protocol的值均为0,因为当protocol为0时,会自动选择type类型对应的默认协议。
- 返回值:当创建套接字成功的时候,该函数返回一个int类型的值,也就是socket描述符,该值大于等于0;而如果创建套接字失败时则返回-1。
3.2 bind()
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
bind()函数用于将一个IP 地址和端口号与一个套接字进行绑定。许多时候内核会帮我们自动绑定一个IP地址与端口号,然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址和端口以等待客户来连接,作为服务器端,这一步绑定的操作是必要的,而作为客户端,则不是必要的,因为内核会帮我们自动选择合适的IP地址与端口号。
bind()函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择。
参数:
sockfd:sockfd是由socket()函数返回的套接字描述符。
my_addr:my_addr是一个指向套接字地址结构的指针。
addrlen:addrlen指定了以addr所指向的地址结构体的字节长度。
若bind()函数绑定成功则返回0,若出错则为-1。
sockaddr 结构体内容如下:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
我们需要填写的IP地址与端口号等信息,都在sa_data连续的14字节信息里面,但这个结构体对用户操作不友好,一般我们在使用的时候都会使用sockaddr_in结构体,sockaddr_in和sockaddr是并列的结构(占用的空间是一样的),指向sockaddr_in的结构体的指针也可以指向sockadd的结构体,并代替它,而且sockaddr_in结构体对用户将更加友好,在使用的时候进行类型转换就可以了。
struct sockaddr_in { short int sin_family; /* 协议族 */ unsigned short int sin_port; /* 端口号 */ struct in_addr sin_addr; /* IP地址 */ unsigned char sin_zero[8]; /* sin_zero是为了让sockaddr与sockaddr_in两个数据结构体保持大小相同而保留的空字节 */ };
sockaddr_in结构体的第一个字段是与sockaddr结构体是一致的,而剩下的字段就是sa_data连续的14字节信息里面的内容,只不过重新定义了成员变量而已,sin_port字段是我们需要填写的端口号信息,sin_addr字段是我们需要填写的IP地址信息,in_addr结构如下图,剩下sin_zero 区域的8字节保留未用。
typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }
// bind()使用示例
struct sockaddr_in server;
bzero(&server, sizeof(server));
// 向server中填入IP和端口号
server.sin_family = AF_INET; //协议族
server.sin_addr.s_addr = htonl(INADDR_ANY); //IP
server.sin_port = htons(6666); //端口号
// 将IP和端口号进行绑定
bind(sockfd, (struct sockaddr*)&server, sizeof(server));
3.3 connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()函数是套接字连接操作,参数与bind()函数相同,对于TCP协议来说,connect()函数操作成功之后代表对应的套接字已与远端主机建立了连接,可以发送与接收数据。
对于UDP协议来说,没有连接的概念,在这里可将其描述为记录远端主机的IP地址与端口号,UDP协议经过connect()函数调用成功之后,在通过sendto()函数发送数据报时不需要指定目的地址、端口,因为此时已经记录到了远端主机的IP地址与端口号。UDP协议还可以给同一个套接字进行多次connect()操作,而TCP协议不可以,TCP只能指定一次connect操作。
3.4 listen()
listen()函数只能在TCP服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在bind()函数之后调用,在accept()函数之前调用,它的函数原型为:
int listen(int sockfd, int backlog);
参数:
sockfd:sockfd是由socket()函数返回的套接字描述符。
backlog:用来描述sockfd的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为TCP连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个backlog告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
3.5 accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
为了能够正常让TCP客户端能正常连接到服务器,服务器必须遵循以下流程处理:
调用socket()函数创建对应的套接字类型。
调用bind()函数将套接字绑定到本地的一个端口地址。
调用listen()函数让服务器进程进入监听状态,等待客户端的连接请求。
调用accept()函数处理到来的连接请求。
accept()函数用于TCP服务器中,等待着远端主机的连接请求,并且建立一个新的TCP连接,在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态,如果队列中没有未完成连接套接字,并且套接字没有标记为非阻塞模式,accept()函数的调用会阻塞应用程序直至与远程主机建立TCP连接;如果一个套接字被标记为非阻塞式而队列中没有未完成连接套接字, 调用accept()函数将立即返回EAGAIN。
3.6 read()/recv()/recvfrom()
一旦客户端与服务器建立好TCP连接之后,我们就可以通过sockfd套接字描述符来收发数据,这与我们读写文件是差不多的操作,接收网络中的数据函数可以是read()、recv()、recvfrom()等。
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:在socket编程中是指定套接字描述符。
buf:指定存放数据的地址。
count:是指定读取的字节数,将读取到的数据保存在缓冲区buf中。
错误代码:
EINTR:在读取到数据前被信号所中断。
EAGAIN:使用O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读。
EIO:输入输出错误,可能是正处于后台进程组进程试图读取其控制终端,但读操作无效,或者被信号SIGTTIN所阻塞, 或者其进程组是孤儿进程组,也可能执行的是读磁盘或者磁带机这样的底层输入输出错误。
EISDIR:fd 指向一个目录。
EBADF:fd不是一个合法的套接字描述符,或者不是为读操作而打开。
EINVAL:fd所连接的对象不可读。
EFAULT:buf 超出用户可访问的地址空间。
read() 从描述符 fd(描述符可以是文件描述符也可以是套接字描述符)中读取 count 字节的数据并放入从 buf 开始的缓冲区中,read()函数调用成功返回读取到的字节数,此返回值受文件剩余字节数限制,当返回值小于指定的字节数时并不意味着错误;这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数据,或者read()函数被信号中断等),出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:指定接收端套接字描述符。
buf:指定一个接收数据的缓冲区,该缓冲区用来存放recv()函数接收到的数据。
len:指定recv()函数拷贝的数据长度。
参数 flags 一般设置为0即可,其他数值定义如下:
MSG_OOB:接收以out-of-band送出的数据。
MSG_PEEK:保持原有数据,就是说接收到的数据并不会被删除,如果再调用recv()函数还会拷贝相同的数据到buf中。
MSG_WAITALL:强迫接收到指定len大小的数据后才能返回,除非有错误或信号产生。
MSG_NOSIGNAL:recv()函数不会被SIGPIPE信号中断,返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。
错误代码:
EBADF:fd 不是一个合法的套接字描述符,或者不是为读操作而打开。
EFAULT:buf 超出用户可访问的地址空间。
ENOTSOCK:参数 s 为一文件描述词, 非socket.
EINTR:在读取到数据前被信号所中断。
EAGAIN:此动作会令进程阻塞, 但参数s的 socket 为不可阻塞。
ENOBUFS:buf内存空间不足。
ENOMEM:内存不足。
EINVAL:传入的参数不正确。
recv()函数会先检查套接字 sockfd 的接收缓冲区,如果 sockfd 接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv()函数就把 sockfd 的接收缓冲中的数据拷贝到 buf 中,但是要注意的是议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv()函数才能把s的接收缓冲中的数据拷贝完。recv()函数仅仅是拷贝数据,真正的接收数据是由协议来完成的,recv函数返回其实际拷贝的字节数。如果recv()函数在拷贝时出错,那么它返回SOCKET_ERROR;如果recv()函数在等待协议接收数据时网络中断了,那么它返回0。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom()前三个参数和recv()一样,后两个参数为输出型参数,会将客户端的IP、端口号等信息填入到这两个参数中。
3.7 send()/sendto()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
无论是客户端还是服务器应用程序都可以用send()函数来向TCP连接的另一端发送数据。
参数:
sockfd:指定发送端套接字描述符。
buf:指定要发送数据的缓冲区。
len:指明实际要发送的数据的字节数。
flags:一般设置为0即可
当调用该函数时,send()函数会先比较待发送数据的长度len和套接字sockfd的发送缓冲的长度。 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;如果len小于或者等于sockfd的发送缓冲区的长度,那么send()函数先检查协议是否正在发送sockfd的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲中的数据或者sockfd的发送缓冲中没有数据,那么send()函数就比较sockfd的发送缓冲区的剩余空间和len。如果len大于剩余空间大小,send()函数就一直等待协议把sockfd的发送缓冲中的数据发送完。如果len小于剩余空间大小,send()函数就仅仅把buf中的数据拷贝到sockfd的发送缓冲区的剩余空间里。
如果send()函数拷贝数据成功,就返回实际copy的字节数,如果send()函数在拷贝数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
注意:send()函数把buf中的数据成功拷贝到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sendto()函数与send()函数类似,但是它会通过 struct sockaddr 指向的 dest_addr 结构体指定要发送给哪个远端主机,在 dest_addr 参数中需要指定远端主机的IP地址、端口号等,而 addrlen 参数则是指定 dest_addr 结构体的字节长度。
3.8 close()
int close(int fd);
close()函数是用于关闭文件描述符,套接字其实也是文件描述符的一种,所以也可以用来关闭套接字,在关闭套接字后,将无法使用对应的套接字描述符,这个函数比较简单,当你不需要使用某个套接字描述符时,就将其关闭即可。
四、UPD客服/服务端实验
服务端代码:
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
:_port(port),
_ip(ip),
_sock(-1)
{}
bool InitServer()
{
// 1. 创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock < 0)
{
std::cout << "socket error" << std::endl;
exit(2);
}
// 2. 将用户设置的ip和port通过bind进行绑定
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 主机序列转网络序列
// 3. 先将字符串风格ip地址转为主机序列,再将主机序列转为网络序列
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_sock, (struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cout << "bind failed" << std::endl;
exit(2);
}
std::cout << "InitServer Success!" << std::endl;
return true;
}
void Start()
{
char buffer[SIZE];
while(true)
{
struct sockaddr_in peer; //输出型参数
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
std::string cmd_echo;
// 读数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct
sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = 0; //当作字符串
uint16_t client_port = ntohs(peer.sin_port); //从网络中获取
std::string client_ip = inet_ntoa(peer.sin_addr);// 网络序列->字符串风格ip
printf("[%s:%d]# %s\n",client_ip.c_str(), client_port, buffer);
}
// 写回数据
sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if(_sock >= 0)
{
close(_sock);
}
}
private:
// 一个服务器,一般需要ip地址和端口号
uint16_t _port;
std::string _ip;
int _sock;
};
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "\nUsage:" << argv[0] << "ip port" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[2]);
std::unique_ptr<UdpServer> svr(new UdpServer(port, argv[1])); //智能指针
svr->InitServer();
svr->Start();
return 0;
}
客户端代码:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "\nUsage:" << argv[0] << "ServerIp ServerPort\n" << std::endl;
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cout << "socket error" << std::endl;
}
std::string message;
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buffer[1024];
while(true)
{
std::cout << "请输入你的信息: ";
std::getline(std::cin, message);
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server,
sizeof(server));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
netstat -anup 可以查看udp协议的网络状态