1. 网络编程基本概念
1.1 IP地址
IP地址本质其实就是一个4字节的无符号整型数。IP地址分为网络标识和主机标识两部分组成。
- 网络标识:标识主机所在的网络
- 主机标识:标识主机的具体地址
根据网络标识和主机标识所占的字节数不同,IP地址一共有5种类型,常用的有以下3种类型:
A类:网络标识占1字节,主机标识占3字节
B类:网络标识占2字节,主机标识占2字节
C类:网络标识占3字节,主机标识占1字节
特殊的IP地址:
- 0.0.0.0 / 0 — 保留,常用于代表“缺省网络”
- 127.0.0.0 / 8 (这里8其实就是用来表代表网络标识占8bit数据位) — 该地址是回环地址,用于本地软件回送测试
- 255.255.255.255 / 32 — 广播地址
私有地址(不在公网使用的,只在内网使用):
- 10.0.0.0 - 10.255.255.255 / 8
- 172.16.0.0 - 172.31.255.255 / 32
- 192.168.0.0 - 192.168.255.255 /24
1.2 子网掩码
子网掩码,也叫地址掩码。本质其实也是一个4字节无符号整型数。子网掩码一般不会单独存在,子网掩码的作用就是用来区分IP地址中的网络标识和主机标识的。例如:
IP地址:192.168.10.250
子网掩码:255.255.255.0
那么可以看出网络标识是:IP地址的前面3字节。主机标识只占1字节。
1.3 端口号
端口号是一个2字节的数据,其作用就是当设备进行网络通信时,用于标识具体连接的是该设备的哪一个应用程序。
1.4 域名
域名其实就是某个某个网站的网址,比如:www.baidu.com ,但是这个网址最终会指向某一个特定的IP地址。因为IP地址是一长串的数字,比较难记住,于是人们发明一串有意义的字符串用来代替这个IP地址。所以,域名可以看作是IP地址的别名,多个域名可以指向同一个IP地址。
但是我们访问某个网址时,最终都要转换为访问某一个特定的IP地址,这就需要域名服务器(DNS)提供帮助。
2. Linux下网络编程常用的API函数
函数原型 | 功能描述 |
---|---|
int socket(int domain, int type, int protocol); | 创建套接字,为网络连接做准备 |
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); | 连接指定地址的远程设备 |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); | 服务端编程专用。作用是socket创建套接字成功后,需要对套接字进行地址和端口的绑定,才能进行数据的收发 |
int listen(int sockfd, int backlog); | 服务端编程专用。监听客户端的连接请求 |
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); | 服务端编程专用。用于接受客户端的连接请求,接收连接成功后,会返回客户端的套接字。如果没有客户端进行连接,那么服务端程序会阻塞在该函数,直到有客户端发起连接请求 |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); | 发送数据到远程设备 |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); | 接收远程设备发回的数据,数据暂存在buf这个缓冲区中 |
int close(int fd); | 关闭连接,销毁套接字 |
in_addr_t inet_addr(const char *cp); | 将IP字符串转换为符合网络字节序的整数 |
int inet_aton(const char *cp, struct in_addr *inp); | 功能和inet_addr函数一样。但是该函数如果转换成功返回1,失败返回0.使用更加的严谨 |
char *inet_ntoa(struct in_addr in); | 将符合网络字节序的整数地址转换为字符串形式 |
3. 服务端例程
服务端的程序是长期暴露于网络的,并等待客户端的连接,而客户端是主动发起连接请求的。所以区分一个程序是服务端还是客户端,就看该程序是否是主动发起连接的。
3.1 服务端编程的流程
对于TCP/IP协议,一般服务端的编程流程如下:
- 调用 socket 函数创建 socket(侦听socket。该socket只用于接收连接,并不进行实际的通信);
- 调用 bind 函数将 socket 绑定到某个IP和端口的二元组上;
- 调用 listen 函数开启侦听状态;
- 调用 accept 函数等待客户端连接,当有客户端连接时,accept函数会返回一个与客户端通信的socket(客户端socket);
- 基于新产生的 socket 调用 send 或 recv 函数开始与客户端进行数据交互;
- 通信结束后,调用 close 函数关闭侦听 socket;
3.2 服务端代码
下面我们来使用Linux提供的API函数,编写一个服务端的例程。该服务端例程完成如下的功能:
- 服务端持续监听客户端的连接;
- 服务端连接了客户端的请求后,接收客户端发送过来的数据,并原样返回去;
- 当服务端接收到客户端发送过来的"quit"字符串,那么服务端断开连接。
代码如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int s_sock = 0;
struct sockaddr_in s_addr = {0};
int c_sock = 0;
struct sockaddr_in c_addr = {0}; // 用于保存客户端的地址信息
socklen_t c_addrlen = 0;
int recv_len = 0;
char recv_buf[64] = {0};
s_sock = socket(PF_INET, SOCK_STREAM, 0);
if( s_sock == -1 )
{
printf("server socket error\n");
return -1;
}
s_addr.sin_family = AF_INET; // 使用IPV4协议族
s_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机上所有的IP
s_addr.sin_port = htons(8888); // 指定端口号
if(bind(s_sock, (const struct sockaddr*)&s_addr, sizeof(s_addr)) == -1)
{
printf("bind error\n");
return -1;
}
if(listen(s_sock, 1) == -1)
{
printf("listen error\n");
return -1;
}
printf("server start success\n");
while( 1 )
{
c_addrlen = sizeof(c_addr);
c_sock = accept(s_sock, (struct sockaddr*)&c_addr, &c_addrlen);
if( c_sock == -1 )
{
printf("client accpet error\n");
return -1;
}
printf("c_sock = %d\n", c_sock);
while( 1 )
{
recv_len = recv(c_sock, recv_buf, sizeof(recv_buf), 0);
printf("receive: %s\n", recv_buf);
if( strcmp(recv_buf, "quit") != 0 )
{// 把接收到的字符原样返回去
send(c_sock, recv_buf, recv_len, 0);
}
else
{// 接收到quit字符串,断开与客户端的连接
break;
}
}
close(c_sock);
}
close(s_sock);
return 0;
}
4. 客户端例程
一般对于TCP/IP协议的客户端程序,编程流程如下:
- 调用 socket函数创建客户端 socket
- 调用 connect 函数尝试连接服务器
- 连接成功以后调用 send 或 recv 函数开始与服务器进行数据交互
- 通信结束后,调用 close 函数关闭socket
下面我们编写客户端例程,主要实现功能如下:
- 接收用户输入的字符串,然后把输入的字符串通过网络发送给服务端;
- 接收服务端的原样返回的数据,并且把接收到的数据打印出来。
代码如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int sock = 0;
struct sockaddr_in addr = {0};
char recv_buf[128] = {0};
char input[32] = {0};
int recv_len = 0;
sock = socket(PF_INET, SOCK_STREAM, 0);
if( sock == -1 )
{
printf("socket error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("192.168.4.102");
addr.sin_port = htons(8888);
if( connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1 )
{
printf("connect error\n");
return -1;
}
printf("connect success\n");
while( 1 )
{
printf("input: ");
scanf("%s", input); // 获取用户输入的字符串
send(sock, input, strlen(input) + 1, 0);
recv_len = recv(sock, recv_buf, sizeof(recv_buf), 0); // 接收服务端返回的数据
if( recv_len > 0 )
{// 把服务端返回的数据打印出来
printf("receive: %s\n", recv_buf);
}
else
{// 服务端没有数据返回,退出程序
break;
}
}
close(sock);
return 0;
}