网络编程的场景: 假设你面前有五座房子(服务器),你要走到其中一座房子的某一间,此时你站在五座房子面前很迷茫,突然,第二座房子上面有人在叫,并且用汉语(TCP/UDP)叫:“我是第二号楼(ip地址),我的房间是1102(端口号)”,那么你就得到了楼号和房间号(获取服务器ip和端口号),就可以去找那个人(连接)。那个人就回房间了,等待你的到来。
Sockt服务器和客户端的开发步骤
- 创建套接字socket
- 为套接字添加信息(IP地址和端口号)
- 绑定套接字socket
- 监听网络连接
- 监听到有客户端接入,接受一个连接
- 数据交互
- 关闭套接字socket,断开连接
需要用到的12个API: socket、bind、inet_aton、inet_ntoa、listen、accept、read、write、send、recv、connect、htons
socket
函数
socket
函数用于创建一个新的套接字,返回一个套接字文件描述符。
原型
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数
domain
(协议族):指定使用的地址族。常见的值包括:AF_INET
:IPv4协议AF_INET6
:IPv6协议AF_UNIX
:本地通信(UNIX域套接字)
type
(套接字类型):指定套接字的类型。常见的值包括:SOCK_STREAM
:提供面向连接的稳定数据传输(TCP)SOCK_DGRAM
:提供数据报文服务(UDP)SOCK_RAW
:提供原始网络协议访问
protocol
:指定使用的协议。一般情况下,传递0
以自动选择合适的协议。
返回值
成功时返回一个新的套接字文件描述符,失败时返回 -1
并设置 errno
来指示错误类型。
示例
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
bind
函数
bind
函数将套接字与特定的地址和端口绑定。对服务器来说,这一步是必不可少的,以便客户端可以通过指定的地址和端口连接到服务器。
原型
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd
:由socket
函数返回的套接字文件描述符。addr
:指向sockaddr
结构的指针,该结构包含了需要绑定的地址和端口信息。具体类型取决于协议族,比如struct sockaddr_in
用于 IPv4。addrlen
:addr
结构的大小(字节数)。
返回值
成功时返回 0
,失败时返回 -1
并设置 errno
来指示错误类型。
示例
// 声明并清零‘sockaddr_in’结构
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
// 设置地址族、端口号、IP地址
my_addr.sin_family = AF_INET; // 使用IPv4地址
my_addr.sin_port = htons(8080); // 绑定端口号 8080
my_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地地址
// 绑定套接字
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
详细地解释一下 sockaddr
结构及其在 bind
函数中的使用:
sockaddr
是一个通用的地址结构,它可以表示多种不同类型的地址。为了方便使用特定协议族的地址,我们通常会使用具体的地址结构,并在需要时将其强制转换为 sockaddr
类型。
对于IPv4地址,我们使用 sockaddr_in
结构。以下是 sockaddr_in
的定义:
struct sockaddr_in {
sa_family_t sin_family; // 地址族 (例如 AF_INET)
in_port_t sin_port; // 端口号 (需要使用 htons 转换为网络字节序)
struct in_addr sin_addr; // IP地址 (in_addr 结构)
char sin_zero[8]; // 填充字段, 使结构大小与 `sockaddr` 对齐
};
in_addr
结构表示一个IPv4地址,它包含一个成员:
struct in_addr {
uint32_t s_addr; // IP地址 (使用 `inet_aton` 或 `INADDR_ANY` 等设置)
};
inet_aton
和 inet_ntoa
inet_aton
和 inet_ntoa
是两个用于处理IP地址的函数,分别用于将点分十进制的字符串格式的IP地址转换为二进制格式,以及将二进制格式的IP地址转换为点分十进制字符串格式。点分十进制(Dotted Decimal Notation)是一种表示IPv4地址的方法,将IP地址表示为四个以点(.)分隔的十进制数,每个十进制数对应一个字节(8位)。
inet_aton
函数
inet_aton
函数用于将点分十进制的字符串格式的IPv4地址转换为二进制格式,并存储在 in_addr
结构中。
原型
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
参数
cp
:指向以点分十进制表示的IP地址字符串(例如 "192.168.1.1")。inp
:指向in_addr
结构的指针,函数将转换后的二进制IP地址存储在此结构中。
返回值
成功时返回非零值(1),失败时返回零(0)。
示例
struct in_addr addr;
inet_aton("192.168.1.1", &addr);
inet_ntoa
函数
inet_ntoa
函数用于将 in_addr
结构中的二进制格式的IP地址转换为点分十进制的字符串格式。
原型
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
参数
in
:包含二进制格式IP地址的in_addr
结构。
返回值
返回指向静态缓冲区中存储的以点分十进制表示的IP地址字符串的指针。
注意
返回的字符串存储在静态缓冲区中,因此在多线程环境中使用时需要注意线程安全问题。
示例
struct in_addr addr;
addr.s_addr = inet_addr("192.168.1.1"); // 或者使用 inet_aton
char *ip_str = inet_ntoa(addr);
printf("IP address: %s\n", ip_str);
listen
函数
listen
函数用于将套接字设置为被动模式,表示这个套接字用于接受来自客户端的连接请求。
原型
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数
sockfd
:套接字文件描述符,指向一个已经绑定了地址的套接字(通过bind
函数)。backlog
:连接队列的最大长度,即等待处理的连接请求的最大数量。如果有更多的连接请求到达,它们可能会被拒绝或者忽略。
返回值
成功时返回0,失败时返回-1,并设置 errno
来指示错误。
示例
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
// 绑定套接字到一个地址和端口
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
// 设置套接字为监听模式,允许最多5个待处理连接
if (listen(sockfd, 5) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
accept
函数
accept
函数用于从监听套接字的连接队列中接受一个连接请求,并返回一个新的套接字文件描述符用于与客户端通信。
原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockfd
:监听套接字文件描述符,通过listen
函数设置为监听模式的套接字。addr
:指向sockaddr
结构的指针,用于存储连接客户端的地址信息。可以为NULL
,表示不关心客户端地址。addrlen
:指向socklen_t
类型的变量,用于存储客户端地址结构的大小。可以为NULL
,表示不关心客户端地址。
返回值
成功时返回新的套接字文件描述符,失败时返回-1,并设置 errno
来指示错误。
示例
struct sockaddr_in cli_addr;
socklen_t clilen = sizeof(cli_addr);
// 从连接队列中接受一个连接请求
int newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
if (newsockfd == -1) {
perror("accept");
close(sockfd);
exit(EXIT_FAILURE);
}
// 现在可以使用 newsockfd 与客户端通信
在Linux网络编程中,read
和 write
函数用于从套接字读取数据和向套接字写入数据。这两个函数是从Unix系统编程中继承过来的,广泛应用于文件I/O和网络I/O操作。
read
函数(从哪里读)
read
函数用于从文件描述符中读取数据。在网络编程中,文件描述符可以是一个套接字。
原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数
fd
:文件描述符(或套接字描述符)。buf
:指向存储读取数据的缓冲区。count
:要读取的最大字节数。
返回值
- 成功时,返回读取的字节数。如果返回值是0,表示已到达文件末尾(对于套接字,表示对端已关闭连接)。
- 失败时,返回-1,并设置
errno
指示错误。
示例
char buffer[256];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0) {
perror("ERROR reading from socket");
}
write
函数(写到哪里去)
write
函数用于向文件描述符中写入数据。在网络编程中,文件描述符可以是一个套接字。
原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数
fd
:文件描述符(或套接字描述符)。buf
:指向要写入数据的缓冲区。count
:要写入的字节数。
返回值
- 成功时,返回写入的字节数。
- 失败时,返回-1,并设置
errno
指示错误。
示例
const char *message = "Hello, World!";
ssize_t n = write(sockfd, message, strlen(message));
if (n < 0) {
perror("ERROR writing to socket");
}
在网络编程中,send
和 recv
函数是用于在套接字上发送和接收数据的更灵活的函数。它们提供了一些额外的选项,可以更精细地控制数据传输行为。
send
函数
send
函数用于向一个连接的套接字发送数据。
原型
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数
sockfd
:要发送数据的套接字描述符。buf
:指向要发送数据的缓冲区。len
:要发送的数据长度。flags
:控制数据传输的标志,可以是以下标志的组合:设置为0,意味着不使用任何特殊选项MSG_OOB
:发送带外数据。MSG_DONTROUTE
:不使用路由表发送数据。MSG_NOSIGNAL
:在向已关闭的连接发送数据时不产生SIGPIPE
信号。
返回值
- 成功时,返回发送的字节数。
- 失败时,返回-1,并设置
errno
指示错误。
示例
const char *message = "Hello, World!";
ssize_t n = send(sockfd, message, strlen(message), 0);
if (n < 0) {
perror("ERROR sending to socket");
}
recv
函数
recv
函数用于从一个连接的套接字接收数据。
原型
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数
sockfd
:要接收数据的套接字描述符。buf
:指向接收数据的缓冲区。len
:接收数据的最大长度。flags
:控制数据接收的标志,可以是以下标志的组合:设置为0,意味着不使用任何特殊选项MSG_OOB
:接收带外数据。MSG_PEEK
:查看数据但不从输入队列中删除。MSG_WAITALL
:等待所有数据到达。
返回值
- 成功时,返回接收的字节数。如果连接被关闭,返回0。
- 失败时,返回-1,并设置
errno
指示错误。
示例
char buffer[256];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n < 0) {
perror("ERROR receiving from socket");
}
connect
函数
在网络编程中,connect
函数是用来在客户端程序中连接服务器的。connect
函数用于将客户端的套接字连接到服务器上的套接字。
原型
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
数
sockfd
:套接字描述符。addr
:指向包含目标地址和端口的struct sockaddr
结构体的指针。addrlen
:地址结构的大小。
返回值
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
以指示错误。
示例
int sockfd;
struct sockaddr_in serv_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
// 初始化服务器地址结构
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("ERROR invalid server IP address");
close(sockfd);
exit(1);
}
// 连接服务器
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
close(sockfd);
exit(1);
}
htons
函数
htons
函数用于将主机字节顺序转换为网络字节顺序。网络通信使用大端字节序,而主机可能使用小端字节序。因此,在设置端口号时,需要将其转换为网络字节顺序。
- h:host(主机)
- t:to(到)
- n:network(网络)
- s:short(短整数)
因此,htons
代表 "host to network short",即将主机字节顺序的短整数(16 位整数)转换为网络字节顺序。
-
Host:主机字节顺序。不同的计算机体系结构可能使用不同的字节顺序来存储数据。常见的有两种:
- 小端序 (Little Endian):最低有效字节存储在最低地址。
- 大端序 (Big Endian):最高有效字节存储在最低地址。
-
To:表示转换的方向。
-
Network:网络字节顺序。互联网协议采用大端序来表示数据。
-
Short:短整数,指 16 位的整数类型。这个函数专门用于处理 16 位的整数。
示例
uint16_t port = 8080;
uint16_t net_port;
net_port = htons(port);
printf("Host order: %d, Network order: %d\n", port, net_port);
CTRL+Z
- 暂停进程,用于将当前前台进程挂起(暂停),并将其放到后台。(还活着)
CTRL+C
- 终止进程,用于强制终止当前前台进程。(死了)
下面演示的是一个服务端和2个客户端之间互相交流,服务端自动回复:
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
int mark = 0;
char msg[128] = {0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if(argc != 3) {
printf("param is not good\n");
exit(-1);
}
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1) {
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &s_addr.sin_addr);
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
listen(s_fd, 10);
int clen = sizeof(struct sockaddr_in);
while(1) {
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if(c_fd == -1) {
perror("accept");
}
mark++;
printf("get connect: %s\n", inet_ntoa(c_addr.sin_addr));
if(fork() == 0) {
if(fork() == 0) {
while(1) {
sprintf(msg, "welcom No.%d client", mark);
write(c_fd, msg, strlen(msg));
sleep(3);
}
}
while(1) {
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1) {
perror("read");
} else if(n_read > 0) {
printf("\nget: %s\n", readBuf);
} else {
printf("client quit\n");
break;
}
}
break;
}
}
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
int tmp;
char msg[128] = {0};
struct sockaddr_in c_addr;
struct sockaddr_in f_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
if(argc != 3) {
printf("param is not good\n");
exit(-1);
}
printf("%d\n", getpid());
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1) {
perror("socket");
exit(-1);
}
f_addr.sin_family = AF_INET;
f_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &f_addr.sin_addr);
if(connect(c_fd, (struct sockaddr *)&f_addr, sizeof(struct sockaddr)) == -1) {
perror("connect");
exit(-1);
}
while(1) {
if(fork() == 0) {
while(1) {
memset(msg, 0, sizeof(msg));
printf("input: ");
fgets(msg, sizeof(msg), stdin);
write(c_fd, msg, strlen(msg));
}
}
while(1) {
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if(n_read == -1) {
perror("read");
exit(-1);
}else{
printf("\nget:%s\n", readBuf);
}
}
}
return 0;
}
运行结果:下面是同一台虚拟机,再下面是虚拟机和手机。