预备知识
网络套接字socket
属于Linux特殊文件类型(管道、套接字、字符设备、块设备)
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)
在通信过程中, 套接字一定是成对出现的
类比前面进程通信中的管道:一个管道掌控着两个文件描述符和一个缓冲区,而一个套接字掌控着一个文件描述符和两个缓冲区
ps:图中画的不恰当,数据流通并不是从一个套接字的发送端到另一个套接字的接收端,而是从一个套接字的文件描述符流向另一个套接字的文件描述符
小端法:(pc本地存储) 高位存高地址,低位存低地址
大端法:(网络存储) 高位存低地址,低位存高地址(更符合人类阅读习惯,比如数字高位在前)
因此为使网络程序具有可移植性,要调用以下函数做网络字节序和主机字节序的转换:
htonl
:本地→网络(针对IP)转换过程:192.168.1.11→string→atoi→int→htonl→网络字节序
htons
:本地→网络 (port)
ntohl
:网络→本地(IP)
ntohs
:网络→本地(Port)
ps:h代表host、n代表network、l代表32位长int用于IP、s代表16位短int用于端口、函数名中间是to
IP地址转换函数
通过封装的函数直接实现针对IP地址的字节序间的转换
int inet_pton(int af, const char *src, void *dst);
本地字节序(string IP)→网络字节序,一般用于客户端绑定服务器的时候
参数:
- af:AF_INET(指IPv4)、AF_INET6(指IPv6)
- src:传入参数,IP地址(点分十进制)
- dst:传出参数,转换后的网络字节序的IP地址
返回值:
- 成功: 1
- 失败:-1,errno
- 异常: 0(说明src指向的不是一个有效的IP地址)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
网络字节序→本地字节序(string IP),一般用于accept
函数
参数:
- af:AF_INET(指IPv4)、AF_INET6(指IPv6)
- src:传入参数,网络字节序IP地址
- dst:传出参数,转换后的本地字节序(string IP)
- size:缓冲区dst的大小
返回值:
- 成功:dst
- 失败:NULL
sockaddr地址结构
IP + port → 在网络环境中唯一标识一个进程
ps:查询文档 man 7 ip
struct sockaddr_in addr; //定义现在有效的类型
//初始化sockaddr结构体
addr.sin_family = AF_INET; //指定地址族(IPv4或6)
addr.sin_port = htons(9527); //指定端口号(转化为网络字节序)
//对第三个成员初始化有下列两种方式
①指定IP地址(一般用于客户端)
inet_pton(AF_INET, "192.157.22.45", &addr.sin_addr.s_addr);
②自动取出系统中有效的任意IP地址(一般用于服务器)
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(fd, (struct sockaddr *)&addr, size); //调用函数时再强转为函数所需类型(因为函数很老了)
※read函数返回值讨论
- >0:实际读到的字节数
- = 0:(表示对端先关闭连接)已经读到结尾,可以调close()函数了
- -1:应进一步判断error的值
- errno == EAGAIN或EWOULDBLOCK:设置了非阻塞方式读,但没有数据到达(需要再次读)
- errno == EINTR:慢速系统调用被中断(需要重启)
- errno == ECONNRESET:连接被重置(需要close(),移除监听队列)
- errno == 其他情况:异常
网络套接字函数
socket模型创建流程图
一个客户端和一个服务器进行通信,需要创建三个套接字
其中两个属于服务器(一个用于监听,一个用于通信),一个属于客户端(用于通信)
socket函数
头文件:#include<sys/socket.h>
int socket(int domain, int type, int protocol);
//创建一个套接字
参数:
- domain:AF_INET、AF_INET6、AF_UNIX //指定选用的IP地址协议
- type:SOCK_STREAM、SOCK_DGRAM //指定数据传输协议:流式协议/报式协议
- protocol:0 //表示所选择的传输协议的代表协议,选0就代表根据前面指定的传输协议自动选择其代表协议(流式协议的代表协议是TCP,报式协议的代表协议是UDP)
返回值:
- 成功:新套接字对应的文件描述符
- 失败: -1,errno
bind函数
头文件:#include<arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
; 给socket绑定一个地址结构(IP+port)
参数:
- sockfd:套接字对应的文件描述符,即socket函数返回值
- addr:传入参数(struct sockaddr *)&addr 地址结构,注意要强转为
struct sockaddr *
类型- struct sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_port = htons(8888);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addrlen:sizeof(addr) 地址结构的大小
返回值:
- 成功:0
- 失败:-1,errno
listen函数
int listen(int sockfd, int backlog);
设置同时与服务器建立连接的上限数(即同时进行3次握手的客户端数量)
参数:
- sockfd:套接字对应的文件描述符,即socket函数返回值
- backlog:上限数值(最大值为128)
返回值:
- 成功:0
- 失败:-1,errno
accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
阻塞等待客户端建立连接,成功则返回一个与客户端成功连接的服务器的socket文件描述符
参数:
- sockfd:套接字对应的文件描述符,即socket函数返回值
- addr:传出参数,成功与服务器建立连接用于通信的那个客户端的地址结构(IP+port)
- addrlen:传入传出参数
- 入:addr的大小
- 出:客户端addr的实际大小
返回值:
- 成功:能与客户端进行数据通信的socket对应的文件描述符
- 失败:-1,errno
connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
使用现有的socket与服务器建立连接
参数:
- sockfd:套接字对应的文件描述符,即socket函数返回值
- struct sockaddr_in srv_addr; 传入参数,服务器的地址结构
- srv_addr.sin_family = AF_INET;
- srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致
- inet_pton(AF_INET, “服务器的IP地址”,&srv_adrr.sin_addr.s_addr);
- addrlen:服务器的地址结构大小
返回值:
- 成功:0
- 失败:-1,errno
ps:如果不使用bind
函数绑定客户端地址结构,则默认采用”隐式绑定”
C/S模型-TCP
TCP通信流程分析
服务器server:
- socket() 创建socket
- bind() 绑定服务器地址结构
- listen() 设置监听上限
- accept() 阻塞监听客户端连接
- read(fd) 读socket获取客户端数据
- toupper() 小→大写(即具体处理事务)
- write(fd) 再由同一个socket向客户端写
- close();
客户端client:
- socket() 创建socket
- connect(); 与服务器建立连接
- write() 写数据到socket
- read() 读处理后的数据
- 处理读取数据
- close()
※编程练习:实现TCP通信
server:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>
#include"wrap.h"
#define SERV_PORT 8000
int main(int argc, char *argv[]) {
char buf[BUFSIZ];
int listen_fd, connect_fd;
socklen_t clit_addr_len;
struct sockaddr_in serv_addr, clit_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
listen_fd = Socket(AF_INET, SOCK_STREAM, 0); //调用自封装函数,将代码逻辑与出错处理解耦
Bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listen_fd, 128);
clit_addr_len = sizeof(clit_addr);
connect_fd = Accept(listen_fd, (struct sockaddr *)&clit_addr, &clit_addr_len);
//输出查看客户端地址结构
char client_ip[1024];
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(clit_addr.sin_port));
int i;
while(1) {
int n = Read(connect_fd, buf, sizeof(buf));
for(i = 0; i < n; i++) buf[i] = toupper(buf[i]);
Write(connect_fd, buf, n);
}
close(listen_fd);
close(connect_fd);
return 0;
}
client:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>
#include"wrap.h"
#define SERV_PORT 8000
int main(int argc, char *argv[]) {
char buf[BUFSIZ];
int clit_fd;
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
clit_fd = Socket(AF_INET, SOCK_STREAM, 0); //不用bind,隐式绑定
Connect(clit_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
int n;
while(1) {
n = Read(STDIN_FILENO, buf, sizeof(buf));
Write(clit_fd, buf, n); //先从终端输入然后发给服务器处理,再读回客户端
n = Read(clit_fd, buf, sizeof(buf));
Write(STDOUT_FILENO, buf, n); //打印到终端展示
}
close(clit_fd);
return 0;
}
出错处理封装函数
把常用函数与错误处理函数封装在一起,在代码中将出错处理与逻辑分离
编译时要把server.c/client.c和wrap.c联合编译
wrap.h:
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
wrap.c:
#include "wrap.h"
void perr_exit(const char *s) {
perror(s);
exit(-1);
}
int Socket(int family, int type, int protocol) {
//函数名首字母改为大写,既与原函数区分开,又能转到manpage
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
//以socket函数举例,完整代码见~/gua/tcp_socket/wrap.c