本文大部分内容是基于 window 来写的,会介绍一小部分 Linux 内容。两者相差不多,既是您是做 Linux 编程,相信看完这篇文章也会对您有很大帮助的
网络编程中接受连接请求的服务器端套接字创建过程可整理如下。
- 第一步:调用 socket 函数创建套接字。
- 第二步:调用 bind 函数分配 IP 地址和端口号。
- 第三步:调用 listen 函数转为可接收请求状态。
- 第四步:调用 accept 函数受理连接请求。
- 第五步:调用 recv/send 交换数据
- 第六步:调用 closesocket 断开连接
网络编程中请求连接的客户端端套接字创建过程可整理如下。
- 第一步:调用 socket 函数创建套接字。
- 第二步:调用 connect 函数请求连接。
- 第三步:调用 recv/send 交换数据
- 第四步:调用 close 断开连接
一、 基于Linux网络编程中I/O数据函数
#include <unistd.h>
ssize_t write(int fd, const void * buf, size_t nbytes);
→成功时返回写入的字节数,失败时返回-1.
→fd 显示数据传输对象的文件描述符。
→buf 保存要传输数据的缓冲地址值
→nbytes 要传输数据的字节数
---------------------------------------------------------------------------------
ssize_t read(int fd, void * buf, size_t nbytes);
→成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回-1.
→fd 显示数据接收对象的文件描述符。
→buf 保存要接收数据的缓冲地址值
→nbytes 要接收数据的字节数
→此函数定义中,size_t 是通过 typedef 声明的 unsigned int 类型。对 ssize_t 来说,
size_t 前面多加的 s 代表 signed,即 ssize_t 是通过 typedef 声明的 signed int类型。
基于 Windows 的 I/O 函数
#include <winsock2.h>
int send(SOCKET s, const char * buf, int len, int flags);
→成功时返回传输字节数,失败时返回 SOCKET_ERROR
→s 表示数据传输对象连接的套接字句柄值
→buf 保存待传输数据的缓冲地址值
→Len 要传输的字节数
→flags 传输数据时用到的多种选项信息
----------------------------------------------------------------
int recv(SOCKET s, const char * buf, int len, int flags);
→成功时返回接收的字节数(收到 EOF 时为 0),失败时返回 SOCKET_ERROR
→s 表示数据接收对象连接的套接字句柄值
→buf 保存接收数据的缓冲地址值
→Len 能够接收的最大字节数
→flags 接收数据时用到的多种选项信息
二、创建套接字函数
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
→成功时返回 socket 句柄,失败时返回 INVALID_SOCKET //invalid[ˈɪnvəlɪd;ɪnˈvælɪd] 无效的
→af 套接字中使用的协议族(protocol family)信息。
→type 套接字数据传输类型信息
→protocol 计算机通信中使用的协议信息
--------------------------------------------------------------------------------------
面向连接的套接字
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
面向消息的套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
名称 |
协议族 |
PF_INET | IPv4 互联网协议族 |
PF_INET6 | IPv6 互联网协议族 |
PF_LOCAL |
本地通信的 UNIX 协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell 协议族 |
套接字类型 | 性质 |
面向链接的套接字 SOCK_STREAM 又称为 TCP 编程 |
传输过程中数据不会消失 按序传输数据 传输的数据不存在数据边界(传输次数和接收次数可以不相等) |
面向消息的套接字SOCK_DGRAM(datagrams) 又称为 UDP 编程 |
强调快速传输而非顺序传输 传输的数据可能丢失也可能损毁 传输的数据与数据边界(传输几次就要接收几次) 限制每次传输的数据大小 |
三、网络地址初始化、分配地址信息和端口
SOCKET servsock;
struct sockaddr_in servAddr;
char * serv_port = "9190";
/*创建服务器端套接字*/
servSock = socket(PF_INIT, SOCK_STREAM, 0);
/*地址信息初始化*/
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INIT;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
/*分配地址信息*/
bind(servSock, (struct sockaddr * )&servAddr, sizeof(servAddr));
下面一次对上面语句进行讲解,首先对bind函数进行详解
#include <winsock2.h>
int bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
→成功时返回 0,失败时返回-1.
→s 套接字标识
→name 是一个 sockaddr 结构指针,该结构中包含了要结合的地址和端口号
→namelen 确定 name 缓冲区的长度
bind 函数中结构体详解
#include <winsock2.h>
struct sockaddr_in
{
sa_family_t sin_family; //地址族(address family)
uint16_t sin_port; //16 位 TCP/UDP 端口号
struct in_addr sin_addr; //32 位IP 地址
char sin_zero[8]; //不使用
};
struct in_addr
{
union
{
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr
/* can be used for most tcp & ip code */
#define s_host S_un.S_un_b.s_b2
/* host on imp */
#define s_net S_un.S_un_b.s_b1
/* network */
#define s_imp S_un.S_un_w.s_w2
/* imp */
#define s_impno S_un.S_un_b.s_b4
/* imp # */
#define s_lh S_un.S_un_b.s_b3
/* logical host */
};
struct sockaddr
{
unsigned short sa_family; //address family, AF_xxx
char sa_data[14]; //14 bytes of protocol address
};
/*
sockaddr_in 是保存 IPv4 地址信息的结构体。而结构体 sockaddr 并非只为 IPv4 设计,
这从保存地址信息的数组 sa_data 长度为 14 字节也可以看出来。因此,结构体 sockaddr
要求在sin_family 中指定地址族信息。为了与 sockaddr 保持一致,sockaddr_in 结构体
中也有地址族信息。
*/
字节序转换(为什么转换请读者自行百度,这里不做介绍)
#include <winsock2.h>
u_short htons(u_short hostshort);
→功 能:把 short型数据从主机字节序转化为网络字节序。
→返回值:16 位的网络排列方式数据
u_long htonl(u_long hostlong);
→功 能:把 无符号长整型型数据从主机字节序转化为网络字节序。
→返回值:32 位的网络排列方式数据
int atoi (const char * str);
→功 能:把 ascll 码转换为网络字节序
→返回值:成功时,函数将转换后的整数作为int值返回。
--------------------------------------------------------
注1:s 指的是 short,l 指的是long。因此 htons 是 h、to、n、s
的组合,即 host to network short
注2: 利用常数 INADDR_ANY 分配服务器地址,可以自动获取运行服务器端的计算机 IP 地址,不必亲自输入。
而且,若统一计算机中已分配多个 IP 地址,则只要端口号一致,就可以从不同 IP 地址接收数据。
四、server/client 建立连接
在详解 TCP/IP 工作方式之前,我认为有必要介绍一下网络传输中我们编程所涉及到的层,这样有助于我们更好的理解。
- 链路层
链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义 LAN、WAN、MAN 等网络标准。若两台主机通过网络进行数据交换,则需要下图所示的物理链接,链路层就负责这些标准。

- IP 层
为了在复杂的网络中传输数据,首先要考虑路径的选择。向目标传输数据需要经过那条路径是 IP 层解决的问题。IP 层本身是面向消息的、不可靠的协议。每次传输数据时会帮我们选择路径,但并不一致。如果传输中发生路径错误,则选择其他路径;如果发生数据丢失或错误,则无法解决。IP 层协议无法应对数据错误。
- TCP 层
TCP 层以 IP 层提供的路径信息为基础完成实际的数据传输,故该层又称为数据传输层(transport)。IP 层只关注一个数据包的传输过程。因此,机试传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身并不可靠。若只利用 IP 层传输数据,则有可能导致后传输的数据包 B 比先传输的数据包 A 提早到达。另外,传输的数据包 A、B、C 中有可能只收到 A 和 C,甚至收到的 C 可能已损毁。而 TCP 正是起到了保证不发生如上问题的作用。

总之,TCP 存在于 IP 层之上,决定主机之间的数据传输方式,TCP 协议确认后向不可靠的 IP 协议赋予可靠性。
#include <winsock2.h>
int listen(SOCKET s, int backlog);
→成功时返回 0,失败时返回 -1
→s 希望进入等待连接请求状态的套接字标识符,传递的标识符套接字参数成为
服务器端套接字/监听套接字。
→backlog 连接请求等待队列的长度,若为 5,则队列长度为 5,表示最多使用 5 个链接请求进入队列。
SOCKET accept(SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen);
→成功时返回创建的套接字标识符,失败时返回-1.
→s 服务器套接字标识符,它应处于监听状态。
→addr 保存发起链接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充
客户端地址信息。
→addrlen 第二个参数 addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填充
入客户端地址长度。
int connect(SOCKET s, struct sockaddr FAR * name, int FAR namelen);
→成功时返回0,失败时返回-1.
→sock 服务器套接字标识符
→addr 保存 目标服务器端地址信息的变量地址值。
→addrlen 第二个参数 addr 结构体变量的长度。
int closesocket(SOCKET s);
→成功时返回创建的套接字标识符,失败时返回-1.
→s 服务器套接字标识符