一.套接字概念
套接字(Socket)是一种用于网络通信的编程接口,它提供了一种机制,使得不同计算机上的应用程序能够通过网络进行通信和交换数据。
套接字可以看作是应用程序和网络之间的端点,它定义了应用程序与网络之间的通信规则和数据格式。通过套接字,应用程序可以创建、连接、发送和接收数据,实现与其他计算机上的应用程序进行通信。
套接字通常基于TCP/IP协议栈进行通信,其中常用的套接字类型有两种:
-
流式套接字(Stream Socket):使用基于流的TCP协议,提供可靠的面向连接的通信。流式套接字通过建立连接,提供可靠的、无差错的数据传输,保证数据的有序性和完整性。
-
数据报套接字(Datagram Socket):使用基于数据报的UDP协议,提供无连接的通信。数据报套接字以短的数据包(数据报)的形式传输数据,适用于实时性要求较高但不需要可靠性的情况。
使用套接字进行通信通常需要进行以下步骤:
-
创建套接字:应用程序通过调用系统API来创建一个套接字。
-
绑定套接字:将套接字与本地网络地址绑定,以便其他应用程序可以通过网络找到它。
-
监听连接请求(对于服务器):在服务器端,套接字会监听传入的连接请求。
-
建立连接(对于客户端):在客户端,套接字会发起连接请求与服务器建立连接。
-
发送和接收数据:已建立连接的套接字可以通过发送和接收数据来进行通信。
-
关闭套接字:通信完成后,应用程序可以关闭套接字来释放资源。
套接字编程提供了灵活和强大的网络通信功能,广泛应用于互联网、网络通信和分布式计算等领域。
网络通信过程中,套接字一定是成对出现的,一个套接字描述符有两个缓冲区,分别是写缓冲区,读缓冲区
二.预备知识
2.1网络字节序
网络字节序(Network Byte Order)是一种规定的数据表示方式,在网络通信中用于保证不同计算机之间的数据交换的正确性和一致性。
在计算机内部,数据一般以主机字节序(Host Byte Order)存储和处理。主机字节序可以是大端字节序(Big-Endian)或小端字节序(Little-Endian),具体取决于计算机体系结构。大端字节序表示高位字节在前、低位字节在后;小端字节序表示低位字节在前、高位字节在后。
为了在不同的计算机之间进行数据交换,需要使用统一的网络字节序。网络字节序采用的是大端字节序,因此在网络通信中,数据会按照从高位到低位的顺序进行传输。
为了方便在不同字节序之间进行转换,可以使用以下函数进行字节序转换:
htons()
:将16位(2字节)主机字节序转换为网络字节序。ntohs()
:将16位(2字节)网络字节序转换为主机字节序。htonl()
:将32位(4字节)主机字节序转换为网络字节序。ntohl()
:将32位(4字节)网络字节序转换为主机字节序。
使用这些函数进行字节序的转换可以确保在不同计算机之间进行数据交换时,数据的字节顺序是一致的。
网络字节序-->大端法-->高位存低地址
主机字节序-->小端法-->高位存高地址
大小端区别
例如十六进制数值 0x12345678,我们可以将其分解成四个字节进行表示。
在大端存储中,高位字节存储在低地址处,低位字节存储在高地址处。因此,0x12 是高位字节,0x34 是次高位字节,0x56
是次低位字节,0x78 是低位字节。所以在大端存储中,表示为:
高地址 -> 低地址:0x12 0x34 0x56 0x78
在小端存储中,低位字节存储在低地址处,高位字节存储在高地址处。因此,0x78 是低位字节,0x56 是次低位字节,0x34是次高位字节,0x12 是高位字节。所以在小端存储中,表示为:
高地址 -> 低地址:0x78 0x56 0x34 0x12
大小端判断
使用 int
和 char
类型的联合体可以进行字节序的判断,而不使用数组。
以下是一个示例代码:
c++
#include <stdio.h>
union EndianTest {
int value;
char byte;
};
int isLittleEndian() {
union EndianTest test;
test.value = 1;
// 判断低字节的值是否为1
return (test.byte == 1);
}
int main() {
if (isLittleEndian()) {
printf("小端字节序 (Little-Endian)\n");
} else {
printf("大端字节序 (Big-Endian)\n");
}
return 0;
}
在这个示例中,我们定义了一个联合体 EndianTest
,它包含一个 int
类型的成员 value
和一个 char
类型的数组成员 byte
。
函数 isLittleEndian()
中,我们将数值 1
赋值给 value
,然后通过检查 byte[0]
的值是否为 1
来判断字节序。如果 byte[0]
的值为 1
,则表示是小端字节序;如果不为 1
,则表示是大端字节序。
通过这个方法,我们可以使用 int
和 char
类型的联合体来判断当前计算机的字节序是大端还是小端。
2.2 IP地址转换函数
主机字节序转换为网络字节序
不常用方法
常用方法
头文件: #include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
声明:
in_addr_t inet_addr(const char *strptr); //该参数是字符串
typedef uint32_t in_addr_t; (通过vi -t 追一下)
struct in_addr {
in_addr_t s_addr;
};
功能: 主机字节序转为网络字节序
参数: const char *strptr: 字符串
返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示),
否则NULL
网络字节序转换为主机字节序
不常用
常用
头文件 : #include <arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
声明: char *inet_ntoa(stuct in_addr inaddr);
功能: 将网络字节序二进制地址转换成主机字节序。
参数: stuct in_addr in addr : 只需传入一个结构体变量
返回值: 返回一个字符指针, 否则NULL;
2.3 sockaddr数据结构
头文件:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
ipv4通信结构体:
struct sockaddr_in {
sa_family_t sin_family; ----协议族
in_port_t sin_port; ----端口
struct in_addr sin_addr; ----ip结构体
};
struct in_addr {
uint32_t s_addr; --ip地址
};
三.网络套接字函数
3.1 soket模型创建流程图
3.2 socket创建套接字
int socket(int domain, int type, int protocol);
头文件: #include <sys/types.h>
#include <sys/socket.h>
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信
AF_INET ipv4
AF_INET6 ipv6
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
protocol:协议 - 填0 自动匹配底层 ,根据type
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功 文件描述符 0 -> 标准输入 1->标准输出 2->标准出错
3->socket
失败 -1,更新errno
3.3 bind绑定套接字
头文件: #include<sys/types.h> #include<sys/socket.h>
#include<netinet/in.h> #include<netinet/ip.h>
功能:绑定协议, IP以及port
声明: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:套接字
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应 结构体-通信当时socket第一个参数确定,需要强转)
addrlen:结构体大小
返回值:成功 0 失败-1,更新errno
通用结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
ipv4通信结构体:
struct sockaddr_in {
sa_family_t sin_family; ----协议族
in_port_t sin_port; ----端口
struct in_addr sin_addr; ----ip结构体
};
struct in_addr {
uint32_t s_addr; --ip地址
};
3.4 listen监听
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
sockfd:套接字
backlog:同一时间可以响应客户端请求链接的最大个数,不能写0.
不同平台可同时链接的数不同,一般写6-8个
返回值:成功 0 失败-1,更新errno
3.5 accept阻等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
Sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值:
成功:文件描述符; //用于通信
失败:-1,更新errno新errno
3.6 recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd ;
buf 存放位置
len 大小
flags 一般填0,相当于read()函数
MSG_DONTWAIT 非阻塞
返回值:
< 0 失败出错 更新errno
==0 表示客户端退出
>0 成功接收的字节个数
3.7 connect
connect:使用现有的socket与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket 函数返回值
addr:传入参数。地址结构(服务器的地址结构)
addlen:addr长度
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定"
3.8 send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:指定要发送数据的socket描述符。
buf:指向要发送数据的缓冲区的指针。
len:指定要发送数据的长度(以字节为单位)。
flags:指定发送操作的标志,如`0`或`MSG_DONTWAIT`等。可以在调用时传递特定的标志,也可以用`0`表示无特殊标志。
返回值:
成功: 发送的字节数,
错误: -1。
需要注意的是,send()
函数并不保证立即将所有数据发送出去,它会尽力将数据发送给远程主机,但在网络条件不理想或发送缓冲区已满的情况下,可能会导致部分数据未能立即发送。
四.案例
利用socket编程。实现一个服务器,客户端发送文本数据给服务器后,服务器将字母大写转小写,小写转大写。转换完成后发送给客户端。客户端输入"quit#"后退出。并且客户端连接后,服务器端打印客户端的ip地址和端口号。
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//1.socket建立文件描述符
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket is err");
}
//2.bind绑定服务器ip地址和端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
int flage = bind(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
if (flage < 0)
{
perror("bind is err");
return -1;
}
//3.listen监听,设置最大同时连接数量
flage = listen(fd, 5);
if (flage < 0)
{
perror("listen is err");
return -1;
}
printf("listern is ok\n");
//4.accept等待客户端连接
int acceptfd = accept(fd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("flage is err");
return -1;
}
printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//5. 不断读取客服端的内容,大小写转换后,发送给客户端
char buf[1024] = {0};
int size = 0;
while (1)
{
flage = recv(acceptfd, buf, sizeof(buf), 0);
if (flage < 0)
{
perror("recv is err");
}
else if (flage == 0)
{
printf("ip:%s is close\n", inet_ntoa(caddr.sin_addr));
}
else
{
size = strlen(buf);
for (int i = 0; i < size; ++i)
{
if (buf[i] >= 'a' && buf[i] <= 'z')
buf[i] = buf[i] + ('A' - 'a');
else
buf[i] = buf[i] + ('a' - 'A');
}
send(acceptfd, buf, sizeof(buf), 0);
}
}
close(fd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//1.socket建立文件描述符
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket is err");
}
//2.connect连接服务器
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
int flage = connect(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
if (flage < 0)
{
perror("connect is err");
}
//3.服务器端不断发送数据,接受服务器转化后的数据
char buf[1024] = {0};
while (1)
{
//memset(buf,0,sizeof(buf));
fgets(buf, sizeof(buf), stdin);
if (strncmp(buf,"quit#",5)==0)
{
break;
}
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
send(fd, buf, sizeof(buf), 0);
flage = recv(fd, buf, sizeof(buf), 0);
if (flage < 0)
{
perror("recv is err");
}
else
{
fprintf(stdout, "%s\n", buf);
}
}
close(fd);
return 0;
}