一、TCP网络编程基础
1.1、套接字编程基础知识
1.1.1、套接字地址结构
- 通用套接字数据结构 ```cpp struct sockaddr { _SOCKADDR_COMMON (sa); char sa_data[14]; };
define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
typedef unsigned short int sa_family_t;
sockaddr的第一个字段实际上就是sa_family_t这个数据结构,而它也是unsigned short int的别名。<br />sockaddr的取值有:
2. 实际使用的套接字数据结构
```cpp
int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
#define __CONST_SOCKADDR_ARG const struct sockaddr *
但是__CONST_SOCKADDR_ARG的结构设置比较复杂,一般使用sockaddr_in这个结构体进行设置
struct sockaddr_in {
__SOCKADDR_COMMON (sin_);
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[
sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t) - sizeof (struct in_addr)
];
};
#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
typedef unsigned short int sa_family_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
sockaddr_in的第一个字段sin_family为协议簇;第二个字段sin_port为端口号;第三个字段sin_addr为IP地址;第四个是保留字段。
- sockaddr和sockaddr_in的关系
可以发现这两个数据结构虽然字段数量不同,但是它们的大小是一样的,所以可以互相转换,数据不会丢失。
1.2、TCP网络编程流程
1.2.1、TCP网络编程架构
TCP一般使用CS结构进行开发。
- 服务端的程序设计模式
第一步调用socket函数,初始化套接字。
第二步调用bind函数,将套接字与端口绑定。
第三步调用listen函数,设置服务器的监听。
第四步调用accept函数,接收客户端的连接。
第五步调用read和write函数,与客户端进行通信。
第六步调用close函数,释放资源。
- 客户端的程序设计模式
第一步调用socket函数,初始化套接字。
第二步调用connect函数,连接服务端。
第三步调用write和read函数,与服务端进行通信。
第四步调用close函数,释放资源。
1.2.2、初始化套接字socket()
#include <sys/socket.h>
#include <sys/types.h>
int socket (int __domain, int __type, int __protocol);
第一个参数是协议簇;第二个参数是协议类型;第三个参数是套接字文件描述符。
第一个参数的取值 | |
---|---|
名称 | 含义 |
PF_UNIX,PF_LOCAL | 本地通信 |
PF_INET | IPv4协议 |
PF_INET6 | IPv6协议 |
PF_IPX | IPX协议 |
PF_NETLINK | 内核用户界面设备 |
PF_X25 | ITU-T X.25 / ISO-8208协议 |
PF_AX25 | Amateur radio AX.25协议 |
PF_ATMPVC | 原始ATM PVC访问 |
PF_APPLETALK | Apple talk |
PF_PACKET | 底层包访问 |
第二个参数的取值 | |
---|---|
SOCK_STREAM | TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输。 |
SOCK_DGRAM | 支持UDP连接。 |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基于连接的数据传输通道,数据长度定长。每次调用读系统调用时需要将全部数据读出。 |
SOCK_RAW | RAW类型,提供原始的网络协议访问。 |
SOCK_RDM | 提供可靠的数据报文,不过数据可能乱序。 |
SOCK_PACKET | 这是专用类型,不能在通用程序中使用。 |
并不是所有的协议簇都实现了这些类型。
第三个参数用于指定某个协议的特定类型,即type类型中的某个类型。但是某个协议通常只有一个类型,所以第三个参数一般设置为0。
通常socket会执行成功,此时的返回值意味着socket的文件描述符,但也有不成功的时候,这时需要通过返回值判断错误。
socket返回值及其含义 | |
---|---|
EACCES | 没有权限建立指定的domain的type的socket |
EAFNOSUPPORT | 不支持所给的地址类型 |
EINVAL | 不支持此协议或协议不可用 |
EMFILE | 进程文件表溢出 |
ENFILE | 已经到达系统允许打开的文件数量,打开文件过多 |
ENOBUFS/ENOMEM | 内存不足 |
EPROTONOSUPPORT | 指定的协议type在domain中不存在 |
1.2.3、绑定地址端口bind()
int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
#define __CONST_SOCKADDR_ARG const struct sockaddr *
第一个参数是socket的描述符,即socket函数的返回值;第二个参数是sockaddr,即地址与端口的信息;第三个参数是sockaddr的长度。
当bind返回值为0的时候,表示绑定成功,-1表示失败。
bind返回值及其含义 | |
---|---|
EADDRINUSE | 给定地址已经使用 |
EBADF | sockfd不合法 |
EINVAL | sockfd已经绑定到其他地址 |
ENPTSOCK | sockfd是一个文件描述符,不是socket描述符 |
EACCES | 地址被保护,权限不足 |
EADDRNOTAVAIL | 接口不存在或者绑定地址不是本地 |
EFAULT | my_addr指针超出用户空间 |
EINVAL | 地址长度错误,或者socket不是AF_UNIX簇 |
ELOOP | 解析my_addr是符号链接过多 |
ENAMETOOLONG | my_addr过长 |
ENOENT | 文件不存在 |
ENOMEM | 内存不足 |
ENOTDIR | 不是目录 |
EROFS | socket节点应该在只读文件系统上 |
1.2.4、绑定本地端口listen()
int listen (int __fd, int __n);
第一个参数是服务端socket的描述符,第二个参数是等待队列最大长度。listen成功执行返回0,否则返回-1。且可以获得err值。
listen()函数的errno值及含义 | |
---|---|
值 | 含义 |
EBADF | 另一个socket已经在同一端口监听 |
EBADF | 参数sockfd不是合法的描述符 |
ENOTSOCK | 参数sockfd不是代表socket的文件描述符 |
EOPNOTSUPP | socket不支持listen操作 |
在接受连接之前,需要用listen来监听端口。listen函数进队SOCK_STREAM或SOCK_SEQPACKET协议有效。
1.2.5、接受一个网络请求accept()函数
int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
# define __SOCKADDR_ARG struct sockaddr *__restrict
第一个参数是服务端socket描述符,第二个参数是out参数,存储客户端地址、端口的指针,第三个参数也是out参数,存储前一个参数的大小。accept成功执行返回客户端socket描述符,否则返回-1。且可以获得err值。
1.2.6、连接目标网络服务器connect()函数
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
#define __CONST_SOCKADDR_ARG const struct sockaddr *
第一个参数是客户端socket描述符,第二个参数是存储着服务端的ip和地址的sockaddr的指针,第三个参数是前一个参数的大小。connect函数执行成功返回0,否则返回-1。也可以获得err值。
connect()函数的errno值及含义 | |
---|---|
EACCES | 在AF_UNIX协议簇中,使用路径名作为标识,EACCES表示目录不可写或不可访问 |
EACCES/EPERM | 用户没有设置广播标志二连接广播地址或连接请求被防火墙限制 |
EADDRINUSE | 本机地址已经在使用 |
EAFNOSUPPORT | 参数serv_addr的域sa_family不正确 |
EAGAIN | 本地端口不足 |
EALREADY | socket是非阻塞类型并且前面的连接没有返回 |
EBADF | 文件描述符不是合法的值 |
ECONNERFUSED | 连接的主机地址没有侦听 |
EFAULT | socket结构地址超出用户空间 |
EINPROGRESS | socket是非阻塞模式,而链接不能立刻返回 |
EINTR | 函数被信号中断 |
EISCONN | socket已经连接 |
ENETUNREACH | 网络不可达 |
ENOTSOCK | 文件描述符不是一个socket |
ETIMEDOUT | 连接超时 |
1.2.7、写入函数write()
ssize_t write (int __fd, const void *__buf, size_t __n);
第一个参数是客户端socket描述符,第二个参数是要写入的数据的缓冲区,第三个参数是缓冲区的长度。返回成功写入的数据长度。
1.2.8、读取函数read()
ssize_t read (int __fd, void *__buf, size_t __nbytes);
第一个参数是客户端socket描述符,第二个参数是要读取的缓冲区,第三个参数是缓冲区的长度。返回成功读取的数据长度。
1.2.9、关闭套接字函数
int shutdown (int __fd, int __how);
第一个参数是要关闭的socket描述符,第二个参数是如何关闭。
- SHUT_RD:值为0,表示切断读,之后不能使用该socket执行读操作。
- SHUT_WR:值为1,表示切断写,之后不能使用该socket执行写操作。
- SHUT_RDWR:值为2,表示切断读写,之后不能使用该socket执行读写操作。
shutdown执行成功返回0,否则返回-1。可以获得errno。
shutdown的errno值及其含义 | |
---|---|
EBADF | 文件描述符不是合法的值 |
ENOTCONN | socket没有连接 |
ENOTSOCK | 第一个参数是一个文件,不是socket |
1.3、服务端/客户端的简单例子
1.3.1、服务端的简单例子
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include <netinet/in.h>
int main(int argc, char const* argv[]) {
int sock = socket(AF_INET, SOCK_STREAM, 0); // 获取socket,设置ipv4协议,tcp协议
sockaddr_in server_addr_opt; //声明服务端配置
server_addr_opt.sin_family = AF_INET; // 设置协议簇
server_addr_opt.sin_port = htons(8080); // 设置端口
server_addr_opt.sin_addr.s_addr = inet_addr("0.0.0.0"); // 设置IP地址
int bind_status = bind(sock, (sockaddr*)(&server_addr_opt), sizeof(struct sockaddr)); //给socket绑定相应信息
listen(sock, 20); // 开启监听,监听队列最大20
sockaddr_in client_addr_opt; // 声明客户端配置
socklen_t sock_size; 声明客户端配置大小
int client_sock = accept(sock, (sockaddr*)(&client_addr_opt), &sock_size); // 接受客户端消息,并设置客户端配置,客户端配置大小
char readBuf[2048]; // 设置读缓冲区
read(client_sock, readBuf, sizeof(buf)); // 读取数据
std::cout << buf << std::endl;
char writeBuf[] = "HelloWorld"; // 设置写缓冲区
write(client_sock, writeBuf, sizeof(writeBuf)); // 写数据
shutdown(sock, SHUT_RDWR); // 关闭socket
return 0;
}
1.3.2、客户端的简单例子
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include <netinet/in.h>
int main(int argc, char const* argv[]) {
int client = socket(AF_INET, SOCK_STREAM, 0); // 获取客户端socket
sockaddr_in clientOpt; // 声明客户端连接配置
clientOpt.sin_addr.s_addr = inet_addr("127.0.0.1");
clientOpt.sin_family = AF_INET;
clientOpt.sin_port = htons(8080);
connect(client, (sockaddr*)(&clientOpt), sizeof(sockaddr_in)); // 开启连接
char wirteBuf[] = "client HelloWorld";
write(client, wirteBuf, sizeof(wirteBuf));
char readBuf[50];
read(client, readBuf, 50);
std::cout << readBuf << std::endl;
shutdown(client, SHUT_RDWR);
return 0;
}