网络编程(tcp和udp)socket

前置处理

大小端转换

网络字节序使用大端法, 主机字节序使用小端法, 我们需要对大小端进行转化

在Linux中定义了相关转化的函数

#include <arpa/inet.h>
//convert values between host and network byte order
// h->host  to  n->net
uint32_t htonl(uint32_t hostlong);					//  常用于主机小端存储ip转为网络大端存储ip
uint16_t htons(uint16_t hostshort);					//  常用于主机小端存储端口转为网络大端存储端口
uint32_t ntohl(uint32_t netlong);					//  常用于网络小端存储ip转为主机大端存储ip
uint16_t ntohs(uint16_t netshort);					//  常用于网络小端存储端口转为主机大端存储端口

// uint32_t: 无符号 int
// uint16_t: 无符号 short int

IP点分十进制转化

目标讲ip地址“xx.xx.xx.xx”转化为十进制

在POSIX 套接字接口设计上提供了结构体in_addrin6_addr, 分别用来存储IPv4和IPv6类型的IP地址( man inet_aton)。以IPv4为例

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// Internet address
struct in_addr {
	in_addr_t   s_addr;  // in_addr_t -> uint32_t -> 无符号int
}

// 将一个点分十进制的IP地址字符串  -> 转换为网络字节序的32位整型数表示。
in_addr_t inet_addr(const char *cp);
// 将一个点分十进制的IP地址字符串  -> 转换为网络字节序的32位整型数表示。
int inet_aton(const char *cp, struct in_addr *inp);
// 将网络地址  -> 转换为点分十进制IP地址的字符串形式。
char *inet_ntoa(struct in_addr in);

image-20240514203617312

sockaddr结构体(内含ip地址和端口信息)

sockaddr 结构体, 这是一种通用的地址结构,它可以通用的描述IPv4和IPv6的结构,而且基本上所有涉及到地址的接口都使用了该类型作为参数。(比如: 上面addrinfo结构体中, sockaddr *ai_addr 参数, 就使用sockaddr类型 )

它直接把一个具体的IP地址和端口信息混在一起, 使用起来过于麻烦; 我们需要更具体的IPV4和IPV6类型, 所以POSIX标准又更进一步的定义了sockaddr_insockaddr_in6分别用于描述IPV4和IPV6类型。 并且, 在需要通用地址参数的函数调用中(例如,bind()connect()accept()等, 他们需要sockaddr类型的参数),我们可以直接将 sockaddr_insockaddr_in6 结构体的指针转换为 sockaddr 类型使用, 这种转换是安全的。

struct sockaddr_in {
   sa_family_t    sin_family; // 地址类型: AF_INET (IPv4)
   in_port_t      sin_port;   // 端口号: 注意in_port_t实际类型short int   (网络字节序)
   struct in_addr sin_addr;   // IP地址:  internet address
};

struct in_addr {
   in_addr_t   s_addr;  // in_addr_t -> uint32_t -> 无符号int
}

TCP通信

流程

image-20240514204508759

socket

#include <sys/types.h>          
#include <sys/socket.h>
//create an endpoint for communication
int socket(
   int domain, // 协议:AF_INET (IPv4)、AF_INET6 (IPV6)....
   int type, 	// 套接字类型: SOCK_STREAM (TCP)、SOCK_DGRAM (UDP)....
   int protocol// 协议:IPPROTO_TCP (TCP)、IPPTOTO_UDP (UDP)...; 当protocol为0时,会自动选择type类型对应的默认协议。
);
// 返回值:  返回值是一个非负整数, 代表一个文件描述符,用于标识创建的套接字,并通过这个描述符进行后续的网络I/O操作。
  • socket函数本质是在内核态中创建了一个对象。这个函数虽然返回一个文件描述符来标识这个对象 但是它并不是通俗意义上的文件对象
  • 包含了进行网络通信所需要的各种信息和状态(Eg: 地址族/Address Family, 类型/Type, 协议/Protocol, 地址/Socket Address …)。 除了这些信息以外, 这个对象中还维护了两个极其重要的缓冲区输入缓冲区/SO_RCVBUF输出缓冲区/SO_SNDBUF, 这两个缓冲区分别用于临时存储从网络接收的数据和待发送到网络的数据。

image-20240514204845190

Bind

#include <sys/types.h> 
#include <sys/socket.h>
//bind a name to a socket
int bind(
   int sockfd,					// socket端点文件描述符
   const struct sockaddr *addr,// 要绑定的IP地址和端口号
   socklen_t addrlen			// 指定的addr代表结构体长度,确保bind函数可以正确解析给定的地址信息:sizeod(addr)
);
//返回值: 成功时返回0。失败返回-1
  • const struct sockaddr *addr参数: 该参数用于提供给socket端点IP和端口信息, 但是sockaddr是一个通用的地址结构,实际使用的时候还是要使用sockaddr_in (IPv4)sockaddr_in6 (IPv6)

  • 在选择端口号设置时, 建议应当避开知名端口号的范围(<1024)。

  • 使用bind 函数时要注意其地址是大端法描述的,可能需要执行强制类型转换。

  • ps1: 一般我们都是给服务端bind, 那么客户端也可以bind吗?

    • 正常来讲客户端不需要bind; 客户端不bind操作系统都会分配一个临时的随机端口, 这已经足够使用了。
    • 当然如果有特殊需求, 也可以对客户端进行bind, 用以指明发送和接收数据的IP和端口。

    ps2: 服务端可不可以不bind?

    • 如果服务端不进行bind操作, 一般操作系统都会分配一个临时的随机端口以供使用, 但是从逻辑上完全没有任何意义, 不允许这样操作。
  • IP设置:

 当服务端设置监听IP地址时,对于IPv4,有几个特殊的IP地址可以使用:

 0.0.0.0
 // 表示服务端愿意接受指向服务器主机的任何IP地址的连接。
 自己主机IP
 // 无需赘述, 最正常操作
 127.0.0.1
 // 这个地址用于测试和开发,仅允许接收来自本机的回环连接。

Listen

使用listen函数对设置好端口和IP的服务端socket端点监听外部连接请求

#include <sys/types.h>         
#include <sys/socket.h>
//listen for connections on a socket
int listen(
   int sockfd, 	// socket端点文件描述符
   int backlog		// 这个参数指定了套接字可以挂起的最大连接数
);
//返回值: 成功返回0, 失败返回-1
  • 一旦启用了listen之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发送和接收缓冲区(回收空间),转而在内核区维护两个队列结构: 半连接队列和全连接队列
  • 如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复,以方便客户端自动重传)

image-20240514205237953

Connect

#include <sys/types.h>
#include <sys/socket.h>
//initiate a connection on a socket
int connect(
   int sockfd,					// socket端点文件描述符
   const struct sockaddr *addr,// 目标服务器的地址和端口信息
   socklen_t addrlen			// 指定的addr代表结构体长度,确保bind函数可以正确解析给定的地址信息
);
// 返回值: 成功0, 失败-1
  • 建立连接: 使用connect函数使客户端服务器发送建立连接请求,初始化一个连接

  • 客户端在调用connect可以不使用bind来指定本地的端口信息,这客户端就会随机选择一个临时端口号来作为源端口。

  • 调用connect预期是完成TCP建立连接的三次握手。 如果服务端未开启对应的端口号或者未监听,则只能收到一个RST回复,并且报错返回的内容 是"Connection refused"。

Accept

#include <sys/types.h> 
#include <sys/socket.h>
// accept a connection on a socket
int accept(
   int sockfd,				// socket端点文件描述符
   struct sockaddr *addr,	// 用来获取连接对端/客户端的地址信息。如果不需要对端的地址信息, 可设参数为NULL
   socklen_t *addrlen		// 用来获取addr结构体的大小。如果使用addr/非NULL,那么addrlen必须设置addr的大小/sizeof(addr);  如果addr是NULL,addrlen也必须是NULL。
);
// 返回值: 成功则返回一个新的套接字文件描述符,用于与客户端通信。失败返回-1。
  • 获取连接: 使用accept函数服务端socket端点的全连接队列中取出一个连接
  • 需要特别注意的是, addrlen参数是一个传入传出参数,所以使用的时候(非NULL)需要主调函数提前分配好内存空间:sizeof(addr)
  • accept 函数由服务端调用,用于从全连接队列中取出下一个已经完成的TCP连接。如果全连接队列为空,那么accept会陷入阻塞。 一旦全连接队列中到来新的连接,此时accept操作就会就绪 (注意: 这种就绪是读就绪)。
  • 当accept执行完了之后,内核会创建一个新的套接字文件对象,该文件对象关联的文件描述符是accept的返回值,文件对象当中最重要的结构是一个发送缓冲区和接收缓冲区,可以用于服务端通过TCP连接发送和接收TCP段。

image-20240514210014504

通过把旧的管理连接队列的套接字称作监听套接字,而新的用于发送和接收TCP段的套接字称作已连接套接字。通常来说,监听套接字会一直存在,负责建立各个不同的TCP连接(只要源IP、源端口、目的IP、目的端口四元组任意一个 字段有区别,就是一个新的TCP连接),而某一条单独的TCP连接则是由其对应的已连接套接字进行数据通信的。

Send和Recv

#include <sys/types.h>
#include <sys/socket.h>
// send a message on a socket
ssize_t send(
   int sockfd, 		// socket端点文件描述符
   const void *buf, 	// 指向要发送数据的缓冲区的指针
   size_t len, 		// buf中数据的长度,以字节为单位
   int flags			// 用于指定发送操作的额外选项: MSG_OOB(发送紧急数据)、MSG_DONTROUTE(不经过路由器直接发送到本地网络上的目的地)...大多数情况下,flags参数设置为0。
);
// 返回值: 成功返回实际发送的字节数。失败返回-1


#include <sys/types.h>
#include <sys/socket.h>
// receive a message from a socket
ssize_t recv(
   int sockfd, 	// socket端点文件描述符
   void *buf, 		// 指向读出数据存放的缓冲区的指针
   size_t len, 	// buf的长度,以字节为单位
   int flags		// 定接收行为的标志位:MSG_PEEK(查看数据但不从系统缓冲区中移除)、MSG_WAITALL(等待所有请求的数据才返回)...大多数情况下,flags设置为0。
);
// 返回值: 成功时返回实际读取的字节数。如果连接已经关闭返回0(对方close: 四次挥手)。读取失败返回-1

客户端OR服务端使用 Send和 Recv用于发送和接收TCP数据

  • Send和 Recv函数只是将数据在用户态空间和内核态的缓冲区之间进行传输。
  • send时将数据拷贝到内核态并不意味着会马上传输,而是由操作系统决定根据合适的时机, 再由内核协议栈按照协议的规范进行分节发送。(通常缓冲区如果数据过多会分节成 MSS的大小,然后根据窗口条件传输到网络层之中)
  • 对于发送和接收数据, 使用Read和Write函数可以实现同样的效果(本质是相同的),相当于flags 参数为0。

image-20240514210301448

需要特别注意的是, send和recv的次数和网络上传输的TCP数据段的数量没有关系,多次的send和recv可能只需要一次TCP段的传输。另外一方面, TCP是一种流式的通信协议,消息是以字节流的方式在信道中传输,这就意味着一个重要的事情, 消息和消息之间是没有 边界的。在不加额外约定的情况下,通信双方并不知道发送和接收到底有没有接收完一个消息,有可能多个消息会在一次传输中被发送和接收(江湖俗称"粘包"),也有有可能一个消息需要多个传输才能被完整的发送和接收(江湖俗称"半包")

TCP通信代码示例

client

#include <testfun.h>

int main()
{
char *sourceIP = "192.168.106.129";
char *sourcePort = "8080";

int socketFd = socket(AF_INET, SOCK_STREAM, 0);

// 方式一: inet_addr 
// 把 点分十进制, 转成in_addr_t类型(网络IP),  把其存储到结构体in_addr类型中
//in_addr_t addrTIP = inet_addr(sourceIP);
//struct in_addr inAddr;
//inAddr.s_addr = addrTIP;

// 方式二: inet_aton
struct in_addr inAddr;
inet_aton(sourceIP, &inAddr);

// 把端口转为int类型
int sourcePortInt = atoi(sourcePort);
// 把端口号: 有主机字节序, 转为网络字节序
int sourcePortNet = htons(sourcePortInt);

// 构建"struct sockaddr"类型
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_addr = inAddr;
socketAddr.sin_port = sourcePortNet;

// 客户端向服务器发起建立连接请求
int res_connect = connect(socketFd, (struct sockaddr *)&socketAddr, sizeof(socketAddr));
ERROR_CHECK(res_connect, -1, "connect");

// 
while(1){
   char buf[60] = {0};

   // 读取标准输入 
   read(STDIN_FILENO, &buf, sizeof(buf)-1);

   // 把标准输入, 发送给服务器
   int res_send = send(socketFd, &buf, sizeof(buf), 0);
   ERROR_CHECK(res_send, -1, "send");

   char buf2[60] = {0}; 
   // 读取对方输入
   int res_recv = recv(socketFd, &buf2, sizeof(buf2), 0);
   ERROR_CHECK(res_recv, -1, "res_recv");
   ERROR_CHECK(res_recv, 0, "other close");

   // 打印到标准输出
   write(STDOUT_FILENO, &buf2, sizeof(buf2));
}
close(socketFd); 

return 0;
}

serve

#include <testfun.h>

int main()
{
char *sourceIP = "192.168.106.129";
char *sourcePort = "8080";

int socketFd = socket(AF_INET, SOCK_STREAM, 0);

// 方式一: inet_addr 
// 把 点分十进制, 转成in_addr_t类型(网络IP),  把其存储到结构体in_addr类型中
//in_addr_t addrTIP = inet_addr(sourceIP);
//struct in_addr inAddr;
//inAddr.s_addr = addrTIP;

// 方式二: inet_aton
struct in_addr inAddr;
inet_aton(sourceIP, &inAddr);

// 把端口转为int类型
int sourcePortInt = atoi(sourcePort);
// 把端口号: 有主机字节序, 转为网络字节序
int sourcePortNet = htons(sourcePortInt);


// 构建"struct sockaddr"类型
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_addr = inAddr;
socketAddr.sin_port = sourcePortNet;

// bind:绑定端口
int res_bind = bind(socketFd,(struct sockaddr *)&socketAddr, sizeof(socketAddr));
ERROR_CHECK(res_bind, -1, "bind");

// listen:监听端口
listen(socketFd, 10);

// accept: 获取连接
int connectFd = accept(socketFd, NULL, NULL);

while(1){

   char buf2[60] = {0}; 
   // 读取对方输入
   int res_recv = recv(connectFd, &buf2, sizeof(buf2), 0);
   ERROR_CHECK(res_recv, -1, "recv");
   ERROR_CHECK(res_recv, 0, "other close");

   // 打印到标准输出
   write(STDOUT_FILENO, &buf2, sizeof(buf2));

   char buf[60] = {0};
   // 读取标准输入 
   read(STDIN_FILENO, &buf, sizeof(buf)-1);

   // 把标准输入, 发送给服务器
   int res_send = send(connectFd, &buf, sizeof(buf), 0);
   ERROR_CHECK(res_send, -1, "send");
}
close(socketFd); 

return 0;
}

结合Select通信

select

#include <sys/select.h>
#include <sys/time.h>
int select(
   int maxfd,					// 最大文件描述符的值加一
   fd_set *readset,			// 结构中包含待检查是否有可读数据的文件描述符集合
   fd_set *writeset,			// 结构中包含待检查是否可以非阻塞写入的文件描述符集合
   fd_set *exceptionset,		// 结构中包含待检查是否有异常条件发生的文件描述符集合
   struct timeval * timeout	// 表示select调用的最长等待时间
);
// 返回值:正数表示就绪的文件描述符数量, 0表示超时时间到了但没有文件描述符就绪, -1表示发生错误

//集合的相关操作如下:
void FD_ZERO(fd_set *fdset); // 将所有fd清零
void FD_SET(int fd, fd_set *fdset); // 增加一个fd
void FD_CLR(int fd, fd_set *fdset); // 删除一个fd
int FD_ISSET(int fd, fd_set *fdset); // 检查fd是否在fdset中被标记为“就绪”; 不在集合中返回零, 在则非零。

ps注意: 在调用select函数之后,文件描述符集合会被修,集合中只保留了那些已经就绪的文件描述符。所以每次调用select前都需要重新初始化和设置这些集合,除非你明确只关心一次select调用的结果。

client

#include <testfun.h>
int main(){
char *sourceIP = "192.168.106.129";
char *sourcePort = "8080";

int socketFd = socket(AF_INET, SOCK_STREAM, 0);

// 方式一: inet_addr 
// 把 点分十进制, 转成in_addr_t类型(网络IP),  把其存储到结构体in_addr类型中
//in_addr_t addrTIP = inet_addr(sourceIP);
//struct in_addr inAddr;
//inAddr.s_addr = addrTIP;

// 方式二: inet_aton
struct in_addr inAddr;
inet_aton(sourceIP, &inAddr);

// 把端口转为int类型
int sourcePortInt = atoi(sourcePort);
// 把端口号: 有主机字节序, 转为网络字节序
int sourcePortNet = htons(sourcePortInt);

// 构建"struct sockaddr"类型
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_addr = inAddr;
socketAddr.sin_port = sourcePortNet;

// 客户端向服务器发起建立连接请求
int res_connect = connect(socketFd, (struct sockaddr *)&socketAddr, sizeof(socketAddr));
ERROR_CHECK(res_connect, -1, "connect");

fd_set read_fd_set;
while(1){
   FD_ZERO(&read_fd_set);
   FD_SET(socketFd, &read_fd_set);
   FD_SET(STDIN_FILENO, &read_fd_set);

   select(socketFd+1, &read_fd_set, NULL, NULL, NULL);

   if(FD_ISSET(STDIN_FILENO, &read_fd_set)){
       char buf[60] = {0};
       // 读取标准输入 
       int res_read = read(STDIN_FILENO, &buf, sizeof(buf)-1);
       if(res_read == 0){
           // 用户输入了EOF字符:在大多数UNIX和Linux系统上,EOF字符默认是Ctrl+D
           break;
       }
       // 把标准输入, 发送给服务器
       int res_send = send(socketFd, &buf, sizeof(buf), 0);
       ERROR_CHECK(res_send, -1, "send");
   }
   if(FD_ISSET(socketFd, &read_fd_set)){
       char buf2[60] = {0}; 
       // 读取对方输入
       int res_recv = recv(socketFd, &buf2, sizeof(buf2), 0);
       ERROR_CHECK(res_recv, -1, "res_recv");
       if(res_recv == 0){
           printf("对方结束 \n");
           break;
       }
       // 打印到标准输出
       write(STDOUT_FILENO, &buf2, sizeof(buf2));
   }
}
close(socketFd); 
return 0;
}

serve

#include <testfun.h>
int main(){
char *sourceIP = "192.168.106.129";
char *sourcePort = "8080";

int socketFd = socket(AF_INET, SOCK_STREAM, 0);

// 方式一: inet_addr 
// 把 点分十进制, 转成in_addr_t类型(网络IP),  把其存储到结构体in_addr类型中
//in_addr_t addrTIP = inet_addr(sourceIP);
//struct in_addr inAddr;
//inAddr.s_addr = addrTIP;

// 方式二: inet_aton
struct in_addr inAddr;
inet_aton(sourceIP, &inAddr);

// 把端口转为int类型
int sourcePortInt = atoi(sourcePort);
// 把端口号: 有主机字节序, 转为网络字节序
int sourcePortNet = htons(sourcePortInt);

// 构建"struct sockaddr"类型
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_addr = inAddr;
socketAddr.sin_port = sourcePortNet;

// bind:绑定端口
int res_bind = bind(socketFd,(struct sockaddr *)&socketAddr, sizeof(socketAddr));
ERROR_CHECK(res_bind, -1, "bind");

// listen:监听端口
listen(socketFd, 10);

// accept: 获取连接
int connectFd = accept(socketFd, NULL, NULL);

fd_set read_fd_set;
while(1){
   FD_ZERO(&read_fd_set);
   FD_SET(connectFd, &read_fd_set);
   FD_SET(STDIN_FILENO, &read_fd_set);
   select(connectFd+1, &read_fd_set, NULL, NULL, NULL);

   if(FD_ISSET(connectFd, &read_fd_set)){
       char buf2[60] = {0}; 
       // 读取对方输入
       int res_recv = recv(connectFd, &buf2, sizeof(buf2), 0);
       ERROR_CHECK(res_recv, -1, "recv");
       // 判断对方是否已经关闭连接
       if(res_recv == 0){
           printf("对方结束 \n");
           break;
       }
       // 打印到标准输出
       write(STDOUT_FILENO, &buf2, sizeof(buf2));
   }
   if(FD_ISSET(STDIN_FILENO, &read_fd_set )){
       char buf[60] = {0};
       // 读取标准输入 
       int res_read = read(STDIN_FILENO, &buf, sizeof(buf)-1);
       // 用户输入了EOF字符:在大多数UNIX和Linux系统上,EOF字符默认是Ctrl+D
       if(res_read == 0){
           break;
       }
       // 把标准输入, 发送给服务器
       int res_send = send(connectFd, &buf, sizeof(buf), 0);
       ERROR_CHECK(res_send, -1, "send");
   }
}
close(connectFd); 
close(socketFd); 
return 0;
}

端口占用

当我们关闭正在运行的服务端之后, 在短时间内尝试重启服务端有可能失败, 这个错误发生的原因是当重启服务端时, 服务端在尝试bind一个网络地址(IP 地址和端口号)到套接字上时,端口号已经被另一个套接字占用。如图:

image-20240514234647939

// 在上一个TCP连接被关闭后,该连接的端口会进入TIME_WAIT状态。TIME_WAIT状态持续2倍的最大报文段生存时间/MSL/Maximum Segment Lifetime,  
// 在这段时间内,即使原来的连接已经关闭,操作系统仍然保留着连接使用的端口号,不允许其他进程绑定到同一个地址和端口上。
// 当新启动的服务端, 在尝试bind同一个未被释放的端口时,显示`端口号已经被占用`

而在实际工作当中, TIME_WAIT状态的存在虽然有可能会提高连接的可靠性,但是一个服务端当中假如存在大量的TIME_WAIT状态,那么服务端的工作能力会极大地受到限制,而取消TIME_WAIT状态其实对可靠性的影响比较小,所以用户可以选择使用setsockopt 函数修改监听套接字的属性,使其可以在TIME_WAIT状态下依然可以bind重复的地址, 重新接收用户端握手请求。 (man 2 setsockopt)

#include <sys/types.h> 
#include <sys/socket.h>
//set options on sockets
int setsockopt(
 int sockfd,			// 套接字文件描述符
 int level,			// 参数指定控制选项的代码层: SOL_SOCKET/套接字层, IPPROTO_TCP, IPPROTO_IP ....
 int optname,			// 设置socket的那个行为: SO_REUSEADDR/重新绑定正在使用中或等待关闭的地址
                       //					SO_KEEPALIVE/保持连接活跃,检测长时间无数据交换的连接
                      // ...(后面还有一些其它项)
 const void *optval,	// 指向包含新选项值的缓冲区的指针。对于布尔SO_REUSEADDR,非零值表示启用,零值表示禁用。
 socklen_t optlen	//  optval缓冲区的长度, 确保函数能正确解释optval指向的值。
);
// 返回值: 成功返回0, 失败返回-1。

ps: setsockopt 函数需要在bind函数之前执行, socket函数之后

EgCode:

socket(...)
// ...
int reuse = 1;
setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// ...
bind(...);

Select断开重连

客户端和服务器之间的连接可能由于各种原因断开。为了维持一个持久的会话和提供不间断的服务体验,当客户端软件尝试重新建立连接的时候。服务器应该启动重连机制,重新连接服务器。我们可以通过Select和Socket模拟这个过程

#include <testfun.h>
int main(){
char *sourceIP = "192.168.106.129";
char *sourcePort = "8080";

int socketFd = socket(AF_INET, SOCK_STREAM, 0);

// 方式一: inet_addr 
// 把 点分十进制, 转成in_addr_t类型(网络IP),  把其存储到结构体in_addr类型中
//in_addr_t addrTIP = inet_addr(sourceIP);
//struct in_addr inAddr;
//inAddr.s_addr = addrTIP;

// 方式二: inet_aton
struct in_addr inAddr;
inet_aton(sourceIP, &inAddr);

// 把端口转为int类型
int sourcePortInt = atoi(sourcePort);
// 把端口号: 有主机字节序, 转为网络字节序
int sourcePortNet = htons(sourcePortInt);

// 构建"struct sockaddr"类型
struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_addr = inAddr;
socketAddr.sin_port = sourcePortNet;

int reuse = 1;
setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

// bind:绑定端口
int res_bind = bind(socketFd,(struct sockaddr *)&socketAddr, sizeof(socketAddr));
ERROR_CHECK(res_bind, -1, "bind");

// listen:监听端口
listen(socketFd, 10);

int connectFd;

fd_set read_fd_set_base;
FD_ZERO(&read_fd_set_base);
FD_SET(socketFd, &read_fd_set_base);
FD_SET(STDIN_FILENO, &read_fd_set_base);

while(1){
   fd_set read_while;
   memcpy(&read_while, &read_fd_set_base, sizeof(read_fd_set_base));

   select(10, &read_while, NULL, NULL, NULL);

   if(FD_ISSET(connectFd, &read_while)){
       char buf2[60] = {0}; 
       // 读取对方输入
       int res_recv = recv(connectFd, &buf2, sizeof(buf2), 0);
       ERROR_CHECK(res_recv, -1, "recv");
       // 判断对方是否已经关闭连接
       if(res_recv == 0){
           printf("--连接断开-- \n");
            // 增加新连接监听; 去除连接和标准输入监听
           FD_CLR(connectFd, &read_fd_set_base);
           FD_CLR(STDIN_FILENO, &read_fd_set_base);
           FD_SET(socketFd, &read_fd_set_base);
           close(connectFd);
           continue;
       }

       // 打印到标准输出
       write(STDOUT_FILENO, &buf2, sizeof(buf2));
   }
   if(FD_ISSET(socketFd, &read_while)){
       // accept: 获取连接
       connectFd = accept(socketFd, NULL, NULL);
       // 增加连接和标准输入监听; 去除新连接监听
       FD_SET(connectFd, &read_fd_set_base);
       FD_SET(STDIN_FILENO, &read_fd_set_base);
       FD_CLR(socketFd, &read_fd_set_base);
       printf("--对方上线-- \n");
   }
   if(FD_ISSET(STDIN_FILENO, &read_while)){
       char buf[60] = {0};
       // 读取标准输入 
       int res_read = read(STDIN_FILENO, &buf, sizeof(buf)-1);
       // 用户输入了EOF字符:在大多数UNIX和Linux系统上,EOF字符默认是Ctrl+D
       if(res_read == 0){
           printf("服务端: 断开连接 \n");
           break;
       }
       // 把标准输入, 发送给服务器
       int res_send = send(connectFd, &buf, sizeof(buf), 0);
       ERROR_CHECK(res_send, -1, "send");
   }
}
close(connectFd); 
close(socketFd); 
return 0;
}

UDP

流程

image-20240514210950868

Sendto和Recvfrom

#include <sys/types.h>
#include <sys/socket.h>
// send a message on a socket
ssize_t sendto(
   int sockfd, 	// socket端点文件描述符
   const void *buf,// 指向要发送数据的缓冲区的指针
   size_t len, 	// buf中数据的长度,以字节为单位
   int flags,		// 发送操作的额外选项: 紧急发送、直发...大多数情况下,flags参数设置为0。
   const struct sockaddr *dest_addr,	// 要发送的目的地址的IP地址和端口号
   socklen_t addrlen					// 指定的dest_addr代表结构体长度,确保bind函数可以正确解析给定的地址信息:sizeod(dest_addr)
);
// 返回值: 成功时返回实际发送的字节数。这个数值可能会小于在length参数中指定的数值,表示只有部分数据被发送。失败时,返回-1。


#include <sys/types.h>
#include <sys/socket.h>
//receive a message from a socket
ssize_t recvfrom(
   int sockfd,		// socket端点文件描述符
   void *buf,		// 指向读出数据存放的缓冲区的指针
   size_t len,		// buf中数据的长度,以字节为单位
   int flags,		// 接收行为的标志位, 默认0
   struct sockaddr *src_addr,	// 用于存储发送方的地址信息
   socklen_t *addrlen			// 指定的src_addr代表结构体长度,确保bind函数可以正确解析给定的地址信息: &sizeod(src_addr)
);
// 返回值: 成功时,返回接收到的字节数; 失败时返回-1。
  • 和基于TCPsend和recv函数不同的是, 基于UDPsendto和recvfrom函数携带了地址信息, 用于确定目的地址和获取接收的信息的来源地址。
  • 这也就意味着, 在使用UDP进行无连接的通信时, 因为没有建立连接的过程,所以必须总是由客户端先调用sendto发送消息给服务端,这样服务端才能知道对端的地址信息,从进入后续的通信。
  • 在使用UDP进行无连接的通信时, 因为是无连接的, 所以客户端或服务端关闭, 对方无法直接感知。
  • 需要注意的是类型和参数: *socklen_t addrlen (非int) (socklen_t 本身是unsigned int)

通信实例

client

#include <testfun.h>
int main(){
char *ip = "192.168.106.129";
char *port = "8080";

struct sockaddr_in socket_addr;
socket_addr.sin_family = AF_INET;
// 字符串-> int -> 大端
socket_addr.sin_port = htons(atoi(port));
// 点分十进制 -> 大端ip
socket_addr.sin_addr.s_addr = inet_addr(ip);

int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

fd_set  read_set_base;
while(1){
   FD_ZERO(&read_set_base);
   FD_SET(STDIN_FILENO, &read_set_base);
   FD_SET(socket_fd, &read_set_base);

   select(socket_fd+1, &read_set_base, NULL, NULL, NULL);
   if(FD_ISSET(STDIN_FILENO, &read_set_base)){
       char buf[60] = {0};
       int res_read = read(STDIN_FILENO, &buf, sizeof(buf));

       int res_send = sendto(socket_fd, &buf, sizeof(buf),0,(struct sockaddr *) &socket_addr, sizeof(socket_addr));
       ERROR_CHECK(res_send, -1, "send");
   }
   if(FD_ISSET(socket_fd, &read_set_base)){
       char buf[60] = {0};
       struct sockaddr_in recv_addr;
       socklen_t len = sizeof(recv_addr);
       int res_recvfrom = recvfrom(socket_fd,&buf,sizeof(buf),0,(struct sockaddr *)&recv_addr,&len);
       printf("from %s: %s \n",inet_ntoa(recv_addr.sin_addr), buf);
   }
}
close(socket_fd);
return 0;
}

serve

#include <testfun.h>
int main(){
char *ip = "192.168.106.129";
char *port = "8080";
struct sockaddr_in socket_addr;
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(atoi(port));
socket_addr.sin_addr.s_addr = inet_addr(ip);

int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
bind(socket_fd,(struct sockaddr *)&socket_addr, sizeof(socket_addr));

struct sockaddr_in client_addr; 
memset(&client_addr, 0, sizeof(client_addr));
fd_set read_set_base;
while(1){
   FD_ZERO(&read_set_base);
   FD_SET(STDIN_FILENO, &read_set_base);
   FD_SET(socket_fd, &read_set_base);

   select(socket_fd+1, &read_set_base, NULL, NULL, NULL);

   if(FD_ISSET(STDIN_FILENO, &read_set_base)){
       char buf[60] = {0};
       read(STDIN_FILENO, &buf, sizeof(buf));
       if(!client_addr.sin_port){
           printf("暂时未知客户端,等待客户端连接重新输入 \n");
       }else{
           int res_send = sendto(socket_fd, &buf, sizeof(buf),0,(struct sockaddr *)&client_addr,sizeof(client_addr));
           printf("res_send: %d \n", res_send);
       }
   }
   if(FD_ISSET(socket_fd, &read_set_base)){
       char buf[60] = {0};
       socklen_t len = sizeof(client_addr);
       recvfrom(socket_fd, &buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);
       printf("from %s:%d 的数据: %s \n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port), buf);
   }
}
close(socket_fd);
return 0;
}

TCP聊天室demo

编写带有超时踢出的聊天室程序:

客户端和服务端使用tcp通信;

服务端可以处理新客户端的连接和转发消息;

客户端可以连入服务端并发送消息。每个客户端只要有30s未活跃,则被踢出聊天室。

client

#include <my_header.h>

int main(int argc, char *argv[])
{                                  
    char *port = "8080";
    char *ip = "192.168.5.128";

    int socket_fd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in sockaddr;
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(atoi(port));
    sockaddr.sin_addr.s_addr = inet_addr(ip);
    int ret_connect = connect(socket_fd,(struct sockaddr *) &sockaddr,sizeof(sockaddr));
    ERROR_CHECK(ret_connect,-1,"connect");
    fd_set set;
    FD_ZERO(&set);
    while(1) {
        FD_SET(STDIN_FILENO,&set);
        FD_SET(socket_fd,&set);
        select(socket_fd + 1,&set,NULL,NULL,NULL);
        if (FD_ISSET(STDIN_FILENO,&set)) {
            // printf("STDIN\n");
            char buf[100] = {0};
            read(STDIN_FILENO,buf,sizeof(buf));
            send(socket_fd,buf,sizeof(buf),0);
        }
        if (FD_ISSET(socket_fd,&set)) {
            // printf("recv\n");
            char buf[100] = {0};
            int ret_recv = recv(socket_fd,buf,sizeof(buf),0);
            if (ret_recv == 0) {
                printf("超时断开\n");
                break;
            }
            printf("other_client : %s",buf);
        }
    }
    close(socket_fd);
    return 0;
}

serve

#include <my_header.h>

typedef struct conn {
    int netfd;
    int isalive;
    time_t active_time;
}conn_t;

int main(int argc, char *argv[])
{                                  
    // 设置端口号,ip号
    char *ip = "192.168.5.128";
    char *port = "8080";
    // 设置socket
    int socket_fd = socket(AF_INET,SOCK_STREAM,0);
    int num = 1;
    setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&num,sizeof(num));
    // 设置sockaddr(内含端口号、ip)
    struct sockaddr_in sockaddr;
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(atoi(port));
    sockaddr.sin_addr.s_addr = inet_addr(ip);
    // bind socket
    int ret_bind = bind(socket_fd,(struct sockaddr *) &sockaddr,sizeof(sockaddr));
    ERROR_CHECK(ret_bind,-1,"bind");
    // listen 监听
    int ret_listen = listen(socket_fd,10);
    ERROR_CHECK(ret_listen,-1,"listen");
    fd_set set;
    FD_ZERO(&set);
    FD_SET(socket_fd,&set);
    conn_t list[10];
    memset(list,0,sizeof(list));
    int index = 0;
    while(1) {
        fd_set temp_set;
        memcpy(&temp_set,&set,sizeof(set));
        struct timeval timev;
        timev.tv_sec = 1;
        timev.tv_usec = 0;
        select(20,&temp_set,NULL,NULL,&timev);
        if (FD_ISSET(socket_fd,&temp_set)) {
            int net_fd = accept(socket_fd,NULL,NULL);
            list[index].netfd = net_fd;
            list[index].isalive = 1;
            time(&list[index].active_time);
            FD_SET(net_fd,&set);
            FD_SET(net_fd,&temp_set);
            index++;
        }
        for (int i = 0;i < index;i++) {
            conn_t con = list[i];
            if (con.isalive == 0 || !FD_ISSET(con.netfd,&temp_set)) {
                continue;
            }
            char buf[100] = {0};
            int ret_recv = recv(con.netfd,buf,sizeof(buf),0);
            if (ret_recv == 0) {
                list[i].isalive = 0;
                FD_CLR(list[i].netfd,&set);
                close(list[i].netfd);
                continue;
            }
            for (int j = 0;j < index; j++) {
                conn_t cur = list[j];
                if (cur.isalive == 0 || i == j) continue;
                send(cur.netfd,buf,sizeof(buf),0);
            }
            time(&list[i].active_time);   
        }
        for (int i = 0; i < index; i++) {
            time_t now = time(NULL);
            if (list[i].isalive == 1 && now - list[i].active_time > 20) {
                list[i].isalive = 0;
                FD_CLR(list[i].netfd,&set);
                close(list[i].netfd);
            }
        }
    }
    close(socket_fd);
    return 0;
}
  • 42
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值