TCP/UDP套接字

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ling_hun_pang_zi/article/details/82185349

TCP/UDP套接字

socket套接字相当于网络通信两端的插座,只要对方的socket和自己的socket有通信连接,双方就可以发送和接收数据。
这里写图片描述

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。

网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节

为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示16 位短整数。
例如:htonl 表示将 32 位的长整数从主机字节序转换为网络字节序。

socket地址的数据类型及相关函数

sockaddr数据结构
这里写图片描述

//sockaddr_in结构
struct sockaddr_in {
    sa_family_t sin_family;
    in_port_t sin_port;
    struct in_addr sin_addr;
};

//in_addr结构
struct in_addr {
    uint32_t s_addr;
};

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

字符串转 in_addr 的函数:

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);

in_addr 转字符串的函数:

char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

介绍一下程序中用到的socket API,这些函数都在sys/socket.h中。

// 创建
int socket(int family, int type, int protocol);

socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果socket()调用出错则返回-1。

对于 IPv4,family 参数指定为 AF_INET。

对于 TCP协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。

如果是 UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。

protocol指定为 0 即可。

// 绑定端口号
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind绑定一个固定的网络地址和端口号。

bind()成功返回 0,失败返回-1。

bind()的作用是将参数 sockfd 和 myaddr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号。

struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度。

程序中对 myaddr 参数是这样初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

首先将整个结构体清零,然后设置地址类型为 AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址,端口号为SERV_PORT,我们定义为 8080。

// 监听
int listen(int sockfd, int backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态,listen()声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。我们一般设置backlog的大小为5。

listen()成功返回 0,失败返回-1。

//接受请求
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握手完成后,服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr 是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数,传入的是调用者提供的缓冲区 cliaddr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 cliaddr 参数传 NULL,表示不关心客户端的地址。

简单的TCP网络程序
这里写图片描述
这里写图片描述
由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。
注意:客户端不是不允许调用 bind(),只是没有必要调用 bind()固定一个端口号,服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

TCP客户端

//建立连接
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户端需要调用 connect()连接服务器,connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。

connect()成功返回 0,出错返回-1。
这里写图片描述

简单的UDP网络程序
这里写图片描述
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/ling_hun_pang_zi/article/details/82185349