TCP编程
顺序图
socket() 函数
socket()函数用于创建一个新的套接字。它是进行网络编程的第一步,因为所有的网络通信都需要通过套接字来进行。
原型:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:指定协议族,对于TCP/IP网络,它通常是AF_INET(IPv4)或AF_INET6(IPv6)。
type:指定套接字类型,对于TCP连接,它通常是SOCK_STREAM。
protocol:指定协议,通常对于AF_INET和SOCK_STREAM,协议为0,表示使用TCP协议。
返回值:成功时返回一个非负整数(套接字描述符),失败时返回-1,并设置errno。
bind() 函数
bind()函数用于将套接字与特定的IP地址和端口号绑定起来。这样,服务器就可以在这个特定的IP地址和端口上监听客户端的连接请求。
原型:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:通过socket()函数创建的套接字描述符。
addr:指向sockaddr结构的指针,该结构包含IP地址和端口号。对于IPv4,通常使用sockaddr_in结构。
addr:采用struct sockaddr的结构体地址,通用结构体
struct sockaddr
{
sa_family_t sa_family;
char sa_data[4];
}
struct sockaddr_in
{ 基于Internel通信结构体
as_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
sin_zero , //填充字节,需清零
}
struct in_addr
{
uint32_t s_addr;
}
addrlen:addr参数指向的地址结构的长度。
返回值:成功时返回0,失败时返回-1并设置errno。
举个例子
其中注释:
- 使用
socket()
函数创建一个TCP套接字,并将其描述符存储在server_fd
中。 - 使用
memset()
函数将server_addr
结构清零,以确保所有未明确设置的字段都被初始化为0。 - 设置
server_addr
结构的sin_family
为AF_INET
,表示使用IPv4地址。 - 将
sin_addr.s_addr
设置为INADDR_ANY
,这表示服务器将监听所有可用的网络接口。 - 使用
htons()
函数将端口号从主机字节序转换为网络字节序,并将其存储在sin_port
中。 - 使用
bind()
函数将套接字server_fd
与server_addr
指定的地址和端口绑定。 - 注意:示例中没有实现监听连接和接受连接的代码,这些通常通过
listen()
和accept()
函数来完成。 - 最后,使用
close()
函数关闭套接字以释放资源。
关于使用bind函数的结构体
在bind()
函数的调用中,你需要将sockaddr_in
结构体的地址(即一个指向该结构体的指针)作为参数传递。你通过取地址操作符&
来获取这个地址,并将其强制转换为struct sockaddr *
类型(因为bind()
函数的原型要求这个参数是struct sockaddr *
类型,这是一个更通用的套接字地址结构体指针,sockaddr_in
是它的一个特例)。
同时,你需要传递结构体的大小作为bind()
函数的第三个参数。这是为了确保函数能够正确地解释传递给它的地址信息。你使用sizeof(server_addr)
来获取这个大小。
listen() 函数
listen() 函数用于将套接字设置为监听状态,以接受连接请求。一旦套接字被设置为监听状态,它就可以接受来自客户端的连接请求。
函数原型:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:是之前通过socket()函数创建的套接字描述符。
backlog:指定了内核应该为相应套接字排队的最大连接个数。当多个客户端同时尝试连接时,这个参数限制了可以处于半连接状态(即收到SYN包,但还未收到客户端确认的ACK包)的TCP连接的数量。注意,实际可排队的连接数可能会小于请求数,这取决于系统限制。(内核中的服务器的套接字fd会维护2个链表:1正在三次握手的客户端链表(数量=2*backlog+1)2已经完成三次握手分配好了的newfd)。如:listen(fd,5);//表示系统允许11(5*2+1)个客户同时进行三次握手
返回值:成功时返回0;失败时返回-1,并设置errno以指示错误。
accept() 函数
accept() 函数用于接受一个连接。当套接字处于监听状态时,accept() 函数会阻塞(除非套接字被设置为非阻塞模式),直到一个连接请求到达。一旦连接被接受,accept() 会创建一个新的套接字描述符,用于与连接的客户端进行通信,而原始的套接字描述符(即传递给listen()的那个)则继续用于监听新的连接请求。
函数原型:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:是之前通过socket()函数创建的并已经通过listen()设置为监听状态的套接字描述符。
addr:是一个指向sockaddr结构的指针,该结构用于返回连接客户端的地址信息(可以用作查找客户机)。如果不需要客户端的地址信息,可以设置为NULL。
addrlen:是一个指向socklen_t的指针,用于传入addr结构的大小,并在函数返回时更新为实际返回的地址信息的大小。
返回值:成功时返回一个新的套接字描述符,用于与连接的客户端通信;失败时返回-1,并设置errno以指示错误。
案例(四个函数一起使用):
关于上述的最后输出的printf("Connection accepted from %s:%d\n"inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
inet_ntoa()
和 ntohs()
是两个用于网络编程的函数,它们分别用于将网络字节序(network byte order)转换为主机字节序(host byte order)的不同表示形式(socket函数介绍中有提到)。
inet_ntoa()
用于将网络地址(通常是IPv4地址)从网络字节序的二进制形式转换为点分十进制字符串表示(例如,将 192.168.1.1
的二进制形式转换为文本字符串 "192.168.1.1"
)。
而 ntohs()
则是专门用于将无符号短整型(通常是端口号)从网络字节序转换为主机字节序。
通过上述学习的函数就可以成功写出一个简单的服务器端的代码,如下
如若要写客户端的代码,还需学习一个connect()函数
connect()函数
在网络编程中,connect 函数是一个非常重要的函数,它用于客户端程序来建立一个到服务器的连接。这个函数通常在套接字(socket)编程中使用,特别是在使用TCP协议时。connect 函数尝试将客户端的套接字与服务器上的套接字(或称为端口)建立连接。
函数原型:
在C语言(以及许多其他支持网络编程的语言)中,connect 函数的原型通常如下所示:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
sockfd:这是由socket函数返回的套接字描述符,代表客户端的套接字。
addr:这是一个指向sockaddr结构(或其变体,如sockaddr_in用于IPv4)的指针,该结构包含了服务器的地址和端口信息。
addrlen:这是addr参数所指向的结构的长度,以字节为单位。这允许connect函数知道它应该读取多少字节的地址信息。
返回值:
成功时,connect函数返回0。
出错时,返回-1,并设置全局变量errno以指示错误类型。
代码如下
实现功能如下
即一发一收,实现数据传递
这个代码可以被优化
下图修改了获取地址,通过这样可以自动搜索地址,无需宏定义地址
原代不能获取客户端的信息,修改之后通过accept()函数获取了客户端的地址与端口号信息
也能实现同样的功能