看完了《TCP/IP网络编程》这本书,现在分批做点总结:
什么是套接字:
百度百科的解释是这样的:
套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
非常非常简单的举例说明下:Socket=Ip address+ TCP/UDP + port
在我看来,套接字就是网络编程的接口,是连接网络的一种工具。用IP地址区分主机、用套接字区分主机里面的程序、用端口号区分套接字
可分配的端口号是0-65535,但0-1023是知名端口号,不能使用;TCP套接字和UDP套接字可以共用同一个端口号
Linux套接字创建过程
网络编程中接受连接请求的套接字创建过程可整理如下:
首先介绍一下
Linux相关知识:
1、底层文件访问(Low-Level File Access)和文件描述符
- 底层;与标准无关的操作系统独立提供的
- 文件描述符(对应于window的句柄):系统分配给文件或套接字的整数
- 文件和套接字一般经过创建过程才会被分配文件描述符,输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。
2、打开文件的函数:
int open(const char *path, int flag);
- 成功时返回文件描述符,失败时返回-1.
- path为文件名的字符串地址(路径信息),flag文件打开模式信息
3、关闭文件的函数
ssize_t write(int fd, const void * buf, size_t nbytes);
- 成功时返回写入的字节数,失败时返回-1
- fd,显示数据传输对象的文件描述符
- buf,保存要传输数据的缓冲地址值
- nbytes,要传输数据的字节数
- size_t是通过typedef声明的unsigned int类型,ssize_t代表signed int,
- s代表signed,ssize_t、size_t都是元数据类型,操作体统定义的数据类型会添加后缀_t
4、创建文件的函数
fd=open("data.txt",O_CREAT|O_WRONLY|O_TRUNC);
创建空文件,只写模式打开,若存在同名文件,则清空文件全部数据
5、读取文件的函数
ssize_t read(int fd, void * buf, size_t nbytes);
- 成功时返回接收的字节数(文件结尾返回0),失败时返回-1
- 文件描述符从3开始以由小到大的顺序编号,0,1,2是分配给标准I/O的描述符
服务器端:
1、socket函数创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 成功时返回文件描述符,失败时返回-1.
- domain,套接字中使用的协议族信息;
- type,套接字数据传输类型信息;
- protocol,计算机间通信中使用的协议信息
- 协议族(Protocol Family): PF_INET,IPV4互联网协议族 ;PF_INET6,IPV6互联网协议族
- PF_LOCAL,本地通信的Unix协议族 PF_PACKET,底层套接字的协议族 PF_IPX,IPX Novell协议族
套接字实际采用的最终协议信息是通过socket函数的第三个函数传递, 在指定协议族范围内通过第一个参数决定第三个参数
套接字类型(Type)
即套接字的数据传输方式,socket第二个参数传递,协议族中可能存在多种数据传输方式
1、面向连接的套接字(基于字节)
向socket第二个参数传递SOCK_STREAM,创建面向连接的套接字
特点:
- 传输过程中数据不会丢失、按序传输数据、传输的数据不存在数据边界,
- 套接字连接必须一一对应
- 收发数据的套接字内部有缓冲(buffer),即字节数组 面向连接的套接字会根据接收端的状态传输数据
如:
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
2、面向消息的套接字
SOCK_DGRAM
- 不可靠、不按顺序、无连接、有数据边界、以高速为目的
- 限制每次传输的数据大小
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
协议的最终选择
socket函数的第三个参数决定最终采用的协议,第三个参数大多取0,除非同一协议族中存在多个数据传输方式相同的协议
2、bind函数分配地址信息(IP地址和端口号)
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
- 成功返回0,失败返回-1。
- sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符
- myaddr 存有地址信息的结构体变量地址值
- adddrien 第二个结构体变量的长度
- 地址信息的表示
- AF_INET,IPV4网络协议中使用的地址族
- AF_INET6,IPV6网络协议中使用的地址族
相关结构体
struct sockaddr_in
{
sa_family_t sin_family;//地址族
uint16_t sin_port;//16位tcp/udp端口号,以网络字节序保存
struct in_addr sin_addr;//32位IP地址,以网络字节序保存
char sin_zero[8];//不使用,必需为0,
为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员
};
struct in_addr
{
in_addr_t s_addr;//32位IPV4地址
};
POSIX是UNIX系列操作系统的可移植操作系统接口,定义了一些其他数据类型
- sa_family_t 地址族
- socklen_t 长度
- in_addr_t IP地址,声明为uint32_t
- in_port_t 端口号,声明为uint16_t
网络字节序与地址变换
不同CPU中,4字节整数型值1在内存空间的保存方式是不同的
字节序和网络字节序:
大端序:高位字节存放到低位地址 【高位字节值存放到低位地址,先存高位值】
小端序:高位字节存放到高位地址
IntelCPU系列用小端
网络字节序统一为大端序
字节序转换:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
h代表主机,n代表网络,s指short,l指long,to就是to
除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题
网络地址的初始化和分配
ockaddr_in中保存的地址信息的成员为32位整数型
#include <arpa/inet.h>
in_addr_t inet_addr(const char * string);
成功返回 32位大端序整数型值,失败时返回INADDR_NONE,还可以检测无效IP
#include <arpa/inet.h>
int inet_aton(const char * string, struct in_addr * addr);
- 成功返回1,失败返回0
- string 含有需转换的IP地址信息的字符串地址值
- addr 将保存转换结果的in_addr结构体变量的地址值
aton和addr函数功能完全相同,利用了in_addr结构体,使用频率更高,但Windows没有此函数
网络地址初始化【主要针对服务器】
struct sockaddr_in addr;
char * serv_ip = "211.217.168.13"; //声明IP地址字符串
char * serv_port = "9190"; //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员置零
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化
利用字符串格式的IP地址和端口号初始化了sockaddr_in结构体变量
客户端地址信息初始化
声明sockaddr_in结构体,初始化为要与之连接的服务器端套接字的IP和端口号,
再调用connect
INADDR_ANY
addr.sin_addr.s_addr = htonl(INADDR_ANY);
自动获取运行服务器端的计算机IP地址
同一计算机可以分配多个IP地址,个数与NIC相等
3、listen函数设置监听【将套接字转化为可接收请求状态】
#include
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
- 成功时返回文件描述符,失败时返回-1.
- sock 服务器套接字的文件描述符
- addr 保存发起连接请求的客户端地址信息的变量地址值
- 调用函数后向传递来的地址变量参数填充客户端地址信息
- addrlen 第二个参数addr结构体的长度,但是存有长度的变量地址。
- 函数调用完成后,该变量即被填入客户端地址长度
函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,套接字自动创建并自动与发起连接的客户端建立连接
客户端:
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
- 成功返回0,失败返回-1。
- sock 客户端套接字的文件描述符
- servaddr 保存目标服务器端地址信息的变量地址值
addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
客户端调用connect函数后,服务器端接收连接请求或中断连接请求才会返回
客户端的IP地址和端口在调用connect函数时自动分配
以下是一份简单的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0); //建立TCP套接字
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr)); //地址信息初始化
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)//请求连接
error_handling("connect() error!");
str_len=read(sock, message, sizeof(message)-1); //读取数据
if(str_len==-1)
error_handling("read() error!");
printf("Message from server : %s \n", message);
close(sock); //关闭套接字
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
Windows套接字创建过程:
1、winsock编程时,首选必须调用WSAStartup函数
#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
成功时返回0,失败时返回非零的错误代码值
wVersionRequested,程序员要用的Winsock版本信息
lpWSAData,WSADATA结构体变量的地址值
套接字版本信息应该准备WORD(unsigned short)类型的,传递给该函数第一个参数wVersionRequested,版本号为1.2,则传递0x0201
可用MAKEWORD宏函数构建WORD型版本信息
MAKEWORD(1, 2);版本为1.2;
2、注销库
#include <winsock2.h>
int WSACleanup(void);
成功时返回0,失败时返回SOCKET_ERROR
调用该函数,winsock相关库将归还给Windows操作系统,多在程序结束之前调用
3、相关函数
创建套接字
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
成功时返回套接字句柄,失败时返回INVALID_SOCKET
分配IP和端口
int bind(SOCKET s, const struct sockaddr * name, int namelen);
成功时返回0,失败返回SOCKET_ERROR
激活套接字,使可接收客户端连接
int listen(SOCKET s,int backlog);
成功时返回0,失败返回SOCKET_ERROR
调用受理客户端连接请求
SOCKET accept(SOCKET s,struct sockaddr * addr, int * addrlen);
成功时返回套接字句柄,失败时返回INVALID_SOCKET
客户端发送连接请求
connect(SOCKET s, const struct sockaddr * name,int namelen);
关闭套接字
int closesocket(SOCKET s);
成功返回0,失败返回SOCKET_ERROR
不同于Linux,Windows的文件句柄相关函数和套接字句柄相关函数是有区别的
4、基于Windows的I/O函数
Windows严格区分套接字I/O函数和文件I/O函数
Windows数据传输函数
int send(SOCKET s, const char * buf, int len, int flags);
成功返回传输字节数,失败返回SOCKET_ERROR
s,数据传输对象连接的套接字句柄值
buf,保存待传输数据的缓冲地址值
len,要传输的字节数
flag,传输数据时用到的多种选项信息
int recv(SOCKET s,const char * buf,int len,int flags);
成功返回接收的字节数(收到EOF返回0),失败返回SOCKET_ERROR
以下是Windows的代码:
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* message);
int main(int argc, char *argv[]) {
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strlen;
if (argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error!");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
strlen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strlen == -1)
ErrorHandling("read() error!");
printf("Message from server:%s\n", message);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char * message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}