Socket编程是通过使用Socket API接口来实现的,这些接口允许开发人员创建网络应用程序,实现数据的传输和通信。下面我们就一些常见的Socket API接口示例和讲解,来实现TCP/IP通信。
① socket():创建套接字
socket(socket_family, socket_type, protocol=0)函数用于创建一个套接字对象。socket_family表示地址族(例如,socket.AF_INET表示IPv4),socket_type表示套接字类型(例如,socket.SOCK_STREAM表示TCP套接字),protocol通常默认为0。
// 创建套接字socket
int ser_socket = socket(AF_INET, SOCK_STREAM, 0);
if (ser_socket == -1)
{
perror("socket");
return -1;
}
②初始化地址结构体
// 初始化地址结构体 // IP地址+PORT端口号
struct sockaddr_in addr;
addr.sin_family = AF_INET; //地址簇
addr.sin_port = 56789; //端口(一般以传参的传进来)
addr.sin_addr.s_addr = inet_addr("192.168.159.128"); //IP地址
// addr.sin_addr.s_addr = htonl(INADDR_ANY); //用特殊的"0.0.0.0"这个IP来绑定本机IP地址
③ bind():绑定本机地址和端口
bind((host, port))函数用于将套接字与本地地址和端口绑定,以便在特定地址和端口上监听连接请求。
// 绑定地址结构体bind
int b = bind(ser_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if(b == -1)
{
perror("bind");
return -1;
}
printf("绑定成功\n");
④ connect():建立连接
connect((host, port))函数用于建立到远程主机的TCP连接。它连接到指定的远程地址和端口。
// 建立连接connect
int c = connect(cli_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (c == -1)
{
perror("connect");
return -1;
}
printf("连接成功\n");
⑤ listen():设置监听端口
listen(backlog)函数用于设置套接字为监听模式,backlog指定了可以等待连接的最大客户端数量。
// 开启监听listen
int l = listen(ser_socket, 3);
if (l == -1)
{
perror("listen");
return -1;
}
printf("监听成功\n");//ser_socket由 待链接套接字 变成 监听套接字
⑥ accept():接受TCP连接
accept()函数用于接受客户端的TCP连接请求,并返回一个新的套接字对象,以便与客户端进行通信。
// 等待连接accept
struct sockaddr_in c_addr; //用来存放客户端链接成功之后的IP加端口
int addrlen = sizeof(c_addr);
int new_socekt = accept(ser_socket, (struct sockaddr *)&c_addr, &addrlen);
if (new_socekt == -1)
{
perror("accept");
return -1;
}
// new_socekt 链接成功之后,用来通信的套接字
printf("客户端[%s][%u]连接成功\n", inet_ntoa(c_addr.sin_addr), c_addr.sin_port);
//客户端的IP跟端口,IP是你客户端本身自带的,但是端口是系统随机分配的
⑦ recv(), read():数据接收
这些函数用于从套接字接收数据。recv()和read()用于TCP套接字,它们接收数据并将其存储在指定的缓冲区中。
int new_socekt = *((int *)arg);
while(1)
{
sem_wait(space);//P操作 空间资源-1
char buf[1024];
bzero(buf, sizeof(buf));
read(new_socekt, buf, sizeof(buf));
printf("服务器接收到消息: %s\n", buf);
sem_post(data);//V操作 数据资源+1
}
⑧ send(), write():数据发送
这些函数用于向套接字发送数据。send()和write()用于TCP套接字,它们将数据从缓冲区发送到目标套接字。
int new_socekt = *((int *)arg);
while(1)
{
sem_wait(data);//P操作 数据资源-1
char buf[1024];
printf("请输入发送给客户端的消息:");
fgets(buf, sizeof(buf), stdin);
write(new_socekt, buf, strlen(buf));
sem_post(space);//V操作 空间资源+1
}
在网络编程中,有时候需要进行网络地址的转换,以及设置和获取套接字的属性。这些操作是网络通信中的基本需求之一。在本部分中,我们将介绍与网络地址转换相关的函数以及如何获取和设置套接字属性。
inet_aton
( )和 inet_addr
( )这两个函数允许将点分十进制字符串形式的IP地址转换为32位网络字节序的二进制地址。
inet_aton(const char *cp, struct in_addr *inp)
:将点分十进制字符串 cp
转换为32位网络字节序的二进制地址,并存储在 inp
结构中。
inet_addr(const char *cp)
:将点分十进制字符串 cp
转换为32位网络字节序的二进制地址,并返回结果。
inet_network
( ) inet_network(const char *cp)
函数用于将点分十进制字符串形式的IP网络地址(例如 "192.168.1.0")转换为32位网络字节序的二进制地址。
inet_ntoa
( ) inet_ntoa(struct in_addr in)
函数用于将32位网络字节序的二进制地址转换为点分十进制字符串形式的IP地址,例如将 “11111111111111111111111111111111
”转换为 "192.168.1.47"。
inet_lnaof
( ) 和 inet_netof
( ) 这两个函数用于从 struct in_addr
结构中提取主机位和网络位的部分。inet_lnaof(struct in_addr in)
返回主机位,inet_netof(struct in_addr in)
返回网络位。
getsockopt
( ) getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen)
函数用于获取套接字 sockfd
的属性。你可以指定属性的级别 level
和选项名 optname
,并将结果存储在 optval
中。optlen
用于指定 optval
缓冲区的大小。
setsockopt
( ) setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
函数用于设置套接字 sockfd
的属性。你可以指定属性的级别 level
和选项名 optname
,并提供要设置的值 optval
以及值的大小 optlen
。
这些函数是在网络编程中非常有用的,它们允许你在应用程序中轻松地进行网络地址的转换和管理套接字属性。网络编程需要处理各种网络相关操作,这些函数提供了一种有效的方式来处理这些需求。无论是将字符串形式的IP地址转换为二进制形式,还是获取和设置套接字的属性,这些函数都是网络程序员的强大工具。
服务器与客户端对比图:
服务器端和客户端分别创建了两个线程,task1
和 task2
,分别用于接收和发送消息。它们之间通过有名信号量 space
和 data
进行同步和通信。在客户端中,通过 connect
连接到服务器。最终实现服务器与客户端相互通信:
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SPACE "/my_space" // 使用唯一的路径名
#define DATA "/my_data"
sem_t *space;
sem_t *data;
//线程任务函数1
void *task1(void *arg)
{
int new_socekt = *((int *)arg);
while(1)
{
sem_wait(space);//P操作 空间资源-1
char buf[1024];
bzero(buf, sizeof(buf));
read(new_socekt, buf, sizeof(buf));
printf("服务器接收到消息: %s\n", buf);
sem_post(data);//V操作 数据资源+1
}
}
//线程任务函数2
void *task2(void *arg)
{
int new_socekt = *((int *)arg);
while(1)
{
sem_wait(data);//P操作 数据资源-1
char buf[1024];
printf("请输入发送给客户端的消息:");
fgets(buf, sizeof(buf), stdin);
write(new_socekt, buf, strlen(buf));
sem_post(space);//V操作 空间资源+1
}
}
int main(int argc, char const *argv[])
{
// 删除已存在的同名信号量(如果有的话)
sem_unlink(SPACE);
sem_unlink(DATA);
// 打开有名信号量
space = sem_open(SPACE, O_CREAT, 0777, 1);
data = sem_open(DATA, O_CREAT, 0777, 0);
// (1)创建套接字socket
int ser_socket = socket(AF_INET, SOCK_STREAM, 0);
if (ser_socket == -1)
{
perror("socket");
return -1;
}
// (2)初始化地址结构体 // IP地址+PORT端口号
struct sockaddr_in addr;
addr.sin_family = AF_INET; //地址簇
addr.sin_port = htons(56789); //端口(一般以传参的传进来)
addr.sin_addr.s_addr = inet_addr("192.168.159.128"); //IP地址
// addr.sin_addr.s_addr = INADDR_ANY; //用特殊的"0.0.0.0"这个IP来绑定本机IP地址
// (3)绑定地址结构体bind
int b = bind(ser_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if(b == -1)
{
perror("bind");
return -1;
}
printf("绑定成功\n");
// (4)开启监听listen
int l = listen(ser_socket, 3);
if (l == -1)
{
perror("listen");
return -1;
}
printf("监听成功\n");//ser_socket由 待链接套接字 变成 监听套接字
while(1)
{
// (5)等待连接accept
struct sockaddr_in c_addr; //用来存放客户端链接成功之后的IP加端口
int addrlen = sizeof(c_addr);
int new_socekt = accept(ser_socket, (struct sockaddr *)&c_addr, &addrlen);
if (new_socekt == -1)
{
perror("accept");
return -1;
}
// new_socekt 链接成功之后,用来通信的套接字
printf("客户端【%s】【%u】连接成功\n", inet_ntoa(c_addr.sin_addr), c_addr.sin_port);
// 创建两个线程,用于双向通信
pthread_t pid1, pid2;
pthread_create(&pid1, NULL, task1, &new_socekt);
pthread_create(&pid2, NULL, task2, &new_socekt);
}
// (7)关闭套接字close/shutdown
close(ser_socket);
// shutdown(new_socekt, SHUT_RDWR);
return 0;
}
client.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SPACE "/my_space" // 使用唯一的路径名
#define DATA "/my_data"
sem_t *space;
sem_t *data;
//线程任务函数1
void *task1(void *arg)
{
int cli_socket = *((int *)arg);
while(1)
{
sem_wait(space);//P操作 空间资源-1
char buf[1024];
printf("请输入消息:");
fgets(buf, sizeof(buf), stdin);
write(cli_socket, buf, strlen(buf));
sem_post(data);//V操作 数据资源+1
}
}
//线程任务函数2
void *task2(void *arg)
{
int cli_socket = *((int *)arg);
while(1)
{
sem_wait(data);//P操作 数据资源-1
char buf[1024];
bzero(buf, sizeof(buf));
read(cli_socket, buf, sizeof(buf));
printf("服务器返回消息: %s\n", buf);
sem_post(space);//V操作 空间资源+1
}
}
int main(int argc, char const *argv[])
{
// 删除已存在的同名信号量(如果有的话)
sem_unlink(SPACE);
sem_unlink(DATA);
// 打开有名信号量
space = sem_open(SPACE, O_CREAT, 0777, 1);
data = sem_open(DATA, O_CREAT, 0777, 0);
// (1)创建套接字socket
int cli_socket = socket(AF_INET, SOCK_STREAM, 0);
if (cli_socket == -1)
{
perror("socket");
return -1;
}
// (2)初始化地址结构体服务器的 // IP地址+PORT端口号
struct sockaddr_in addr;
addr.sin_family = AF_INET; //地址簇
addr.sin_port = htons(56789); //服务器端的端口(一般以传参的传进来)
addr.sin_addr.s_addr = inet_addr("192.168.159.128"); //服务器端的IP地址(一般以传参的传进来)
// (3)建立连接connect
int c = connect(cli_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if (c == -1)
{
perror("connect");
return -1;
}
printf("连接成功\n");
// 创建两个线程,用于双向通信
pthread_t pid1, pid2;
pthread_create(&pid1, NULL, task1, &cli_socket);
pthread_create(&pid2, NULL, task2, &cli_socket);
while(1);
// (5)关闭套接字close/shutdown
close(cli_socket);
// shutdown(cli_socket, SHUT_RDWR);
return 0;
}
如果遇到 bind:地址已被使用,我们只需在服务器初始化addr之前加上这段代码,来实现地址的复用即可。
int reuse 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(void *)&reuse,sizeof(reuse))<0)
{
perror("setsockopt error");
return -1;
}
TCP实现可靠传输的关键机制包括:
TCP使用拥塞控制算法来应对网络拥塞情况,以防止网络丢包率过高、延迟增加等问题。主要的拥塞控制算法包括:
慢开始(Slow Start):在连接初始阶段,发送端发送的数据段数量较小,随着时间的推移逐渐增加,以便观察网络状况。
拥塞避免(Congestion Avoidance):一旦慢开始阶段完成,TCP进入拥塞避免阶段,每个传输轮次中发送的数据段数量逐渐增加,直到发生拥塞。
快重传(Fast Retransmit):如果接收端连续收到相同序列号的数据段(表明某个数据段丢失),它会立即向发送端发送重复确认,触发发送端的快速重传。
快恢复(Fast Recovery):当发送端接收到快重传的重复确认时,它不会进入慢开始,而是将拥塞窗口减半,然后继续逐渐增加。
这些算法共同确保TCP在遇到网络拥塞时能够降低发送速率,以减少网络负载,同时也通过快速重传和快速恢复来尽快恢复到正常传输状态。TCP的拥塞控制机制是维护网络稳定性和性能的关键部分。
更多C/C++语言、Linux系统、数据结构和ARM板实战相关文章,关注专栏:
手撕C语言
玩转linux
脚踢数据结构
系统、网络编程
探索C++
6818(ARM)开发板实战
一键三连喔
~