目录
服务器端
第一步:创建用于监听的套接字
第二步:将套接字与本机IP地址和端口号绑定
第三步:设置监听
第四步:等待并接收连接请求
第五步:C/S网络通信
第六步:关闭文件描述符
客户端
第一步:创建套接字
第二步:连接服务器
第三步:通信
服务器端
第一步:创建用于监听的套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址族,常用有AF_INET和AF_INET6,分别代表IPv4和IPv6
type:数据传输方式/套接字类型,常用SOCK_STREAM(流格式套接字/面向连接的套接字),SOCK_DGRAM(数据报套接字/无连接的套接字)
protocol:传输协议,常用IPPROTO_TCP,IPPROTO_UDP,分贝代表TCP和UDP协议
type与protocol不能冲突,即如果使用面向连接的套接字,那么protocol就得是TCP协议。通常protocol参数可以直接填0,这样系统会自动推导出要使用得协议。
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
返回值:成功返回文件描述符(当前进程文件描述符表中最小可用的文件描述符),失败返回-1并设置errno。
第二步:将套接字与本机IP地址和端口号绑定
因为套接字相当于是一个在内核中打开的文件,也就是内存中的一篇缓冲区,现在我们要将该缓冲区与本地IP地址和端口号绑定。
首先将IP地址和端口号放入sockaddr_in结构体变量之中
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
首先定义结构体变量:
struct sockaddr_in serv_addr;
sin_family是使用的地址协议族,一般是AF_INET,即IPv4协议
serv_addr.sin_family = AF_INET;
sin_port是端口号,htons()函数表示将16位的短整数从主机字节序变成网络字节序(即大端模式)。
serv_addr.sin_port = htons(9999); // 本地端口,25536以下
sin_addr存储IP地址,但是不是直接存储成整型数之类的,而是存储到in_addr结构体中。
in_addr结构体如下,可以保存一个32位的IP地址,明明可以直接用,还非要放到一个结构体中,据说是上古时期实现这个东西的程序员想多了。
struct in_addr {
unsigned long s_addr;
};
这里INADDR_ANY即0.0.0.0,即任意地址,因为有些主机有多块网卡可以连到多个网络有不同ip,这样设置可以让服务器程序监听多个网卡ip地址的端口,也可以理解为用多个ip地址监听同一个端口。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
sin_zero是为了让sockaddr结构体与sockaddr_in结构体大小相同,而特意保留的空字节。
将IP地址和端口号放入sockaddr_in结构体变量之后就可以用bind函数将套接字变量和ip,端口号绑定。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd:用于监听的套接字文件描述符
addr:是sockaddr结构体类型的变量地址,前边我们是用sockaddr_in结构体来设置的IP地址和端口号,需要强制类型转换成为sockaddr类型的地址。之所以不直接用sockaddr来设置IP地址和端口号是有原因的,struct sockaddr是通用的套接字地址形式,而不同的网络体系表示地址的方法是不同的,struct sockaddr_in是在通用的套接字地址形式的基础上,针对TCP/IP设计的套接字的地址形式,里面多了两个成员,也就是IP号和端口号,这样方便我们填充。
addrlen:sockaddr_in结构体变量的大小。
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
成功返回0,失败返回-1并设置errno
第三步:设置监听
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:文件描述符
backlog:未完成连接队列和已完成连接队列的上限,暂时理解为对客户端连接数量的限制。
成功返回0,失败返回-1
ret = listen(lfd, 64);
第四步:等待并接收连接请求
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:用于监听的套接字文件描述符
addr:sockaddr类型的传出参数,存放客户端的套接字变量信息
addrlen:addr的大小
addr与addrlen都是可选参数
返回值:
如果监听的队列中无等待连接,且套接字为阻塞,则等待连接到来
如果监听的队列中无等待连接,且套接字为非阻塞,则返回-1并设置errno
如果监听的队列中有等待连接,则取出第一个连接,返回一个与用于监听的套接字同等类型的套接字(本质是一个文件描述符,用于通信)。
// 等待并接收请求连接请求
struct sockaddr_in cline_addr;
socklen_t clien_len = sizeof(cline_addr);
int cfd = accept(lfd, (struct sockaddr*)&cline_addr, &clien_len);
if(cfd == -1){
perror("accept error");
exit(1);
}
接收到连接请求之后可以输出展示一下,注意要把ip地址的格式转化成字符串输出
#include <arpe/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr); //将点分十进制的ip地址转化为用于网络传输的数值格式
返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将数值格式转化为点分十进制的ip地址格式
返回值:若成功则为指向结构的指针,若出错则为NULL
char ipbuf[64];
printf("client ip: %s, port: %d\n", inet_ntop(AF_INET, &cline_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), ntohs(cline_addr.sin_port));
第五步:C/S网络通信
上边的过程只是通过套接字将服务器端和客户端连接了起来,具体的通信就是服务器端进程和客户端进程之间的通信,与普通的unix IO并没有区别,也是通过文件描述符与相关的系统调用进行读写。即像操作普通文件一样用read来读取客户端发送的内容,用write来向客户端写内容。
第六步:关闭文件描述符
关闭用于监听和用于通信的文件描述符。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
int main(int argc, const char* argv[]){
// 创建用于监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socker error");
exit(1);
}
// 套接字与IP和端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999); // 本地端口,25536以下
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定IP?
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret == -1){
perror("bind error");
exit(1);
}
// 设置监听
ret = listen(lfd, 64);
if(ret == -1){
perror("listen error");
exit(1);
}
// 等待并接收请求连接请求
struct sockaddr_in cline_addr;
socklen_t clien_len = sizeof(cline_addr);
int cfd = accept(lfd, (struct sockaddr*)&cline_addr, &clien_len);
if(cfd == -1){
perror("accept error");
exit(1);
}
char ipbuf[64];
printf("client ip: %s, port: %d\n", inet_ntop(AF_INET, &cline_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), ntohs(cline_addr.sin_port));
// 通信
while(1){
// 先接收数据
char buf[1024] = {0};
int len = read(cfd, buf, sizeof(buf));
if(len == -1){
perror("read error");
break;
}else if(len > 0){
// 顺利读出数据
printf("read buf = %s\n", buf);
// 数据小写变大写
for(int i = 0; i < len; ++i){
buf[i] = toupper(buf[i]);
}
printf("大写转换结果:%s\n", buf);
// 大写数据发回给客户端
write(cfd, buf, strlen(buf) + 1);
}else if(len == 0){
printf("client disconnect \n");
break;
}
}
close(lfd);
close(cfd);
}
客户端
第一步:创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
第二步:连接服务器
先定义sockaddr_in结构体变量来设置ip和端口号
注意这里的ip是服务器端的ip,端口号也是服务器进程对应的端口号
inet_pton用来设置ip地址,可以把字符串变成二进制网络字节序,这里的“127.0.0.1”是本地环回地址,因为我们的服务器进程也在本机上,因此要使用本地环回。
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999); // 在服务器进程中设置的端口?
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
接下来是使用connect函数连接服务器,当使用tcp协议的时候,connect会完成三次握手的过程
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd:客户端进程套接字的文件描述符
addr:服务端的ip和端口号结构变量地址,但是需要注意强转,因为我们定义的都是sockaddr_in变量,而使用的参数类型是sockaddr类型的。
addrlen:结构变量的大小
成功返回0,失败返回-1
int ret = connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret == -1){
perror("connect error");
exit(1);
}
第三步:通信
在服务端进程中,监听和通信用的是两个不同的套接字,而在客户端只需要一个通信套接字就可以,不需要监听的。
注意strlen()函数计算字符串长度是不包括最后的‘\0’
while(1){
// 写数据
// 接收键盘输入
char buf[512];
fgets(buf, sizeof(buf), stdin);
// 发送给服务器
write(fd, buf, strlen(buf) + 1);
// 接收服务器端的数据
int len = read(fd, buf, sizeof(buf));
printf("read buf = %s, len = %d\n", buf, len);
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
int main(int argc, const char* argv[]){
// 创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socker error");
exit(1);
}
// 连接服务器
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999); // 在服务器进程中设置的端口?
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret == -1){
perror("connect error");
exit(1);
}
// 通信
while(1){
// 写数据
// 接收键盘输入
char buf[512];
fgets(buf, sizeof(buf), stdin);
// 发送给服务器
write(fd, buf, strlen(buf) + 1);
// 接收服务器端的数据
int len = read(fd, buf, sizeof(buf));
printf("read buf = %s, len = %d\n", buf, len);
}
return 0;
}