Windows socket网络编程 流程示例
TCP与UDP的区别
基于连接与无连接
对系统资源的要求(TCP较多,UDP少)
UDP程序结构较简单
流模式与数据报模式
TCP保证数据正确性,UDP可能丢包
TCP保证数据顺序,UDP不保证
部分满足以下几点要求时,应该采用UDP 面向数据报方式 网络数据大多为短消息
拥有大量Client
对数据安全性无特殊要求
网络负担非常重,但对响应速度要求高
具体编程时的区别 socket()的参数不同
UDP Server不需要调用listen和accept
UDP收发数据用sendto/recvfrom函数
TCP:地址信息在connect/accept时确定
UDP:在sendto/recvfrom函数中每次均 需指定地址信息
UDP:shutdown函数无效
man----socket
通过查看socket的man手册可以看到socket函数的第一个参数的值可以为下面这些值:
Name Purpose
PF_UNIX, PF_LOCAL Local communication
PF_INET IPv4 Internet protocols
PF_INET6 IPv6 Internet protocols
PF_IPX IPX - Novell protocols
PF_NETLINK Kernel user interface device
PF_X25 ITU-T X.25 / ISO-8208 protocol
PF_AX25 Amateur radio AX.25 protocol
PF_ATMPVC Access to raw ATM PVCs
PF_APPLETALK Appletalk
PF_PACKET Low level packet interface
编程区别
通常我们在说到网络编程时默认是指TCP编程,即用前面提到的socket函数创建一个socket用于TCP通讯,函数参数我们通常填为SOCK_STREAM。即socket(PF_INET, SOCK_STREAM, 0),这表示建立一个socket用于流式网络通讯。
SOCK_STREAM这种的特点是面向连接的,即每次收发数据之前必须通过connect建立连接,也是双向的,即任何一方都可以收发数据,协议本身提供了一些保障机制保证它是可靠的、有序的,即每个包按照发送的顺序到达接收方。
而SOCK_DGRAM这种是User Datagram Protocol协议的网络通讯,它是无连接的,不可靠的,因为通讯双方发送数据后不知道对方是否已经收到数据,是否正常收到数据。任何一方建立一个socket以后就可以用sendto发送数据,也可以用recvfrom接收数据。根本不关心对方是否存在,是否发送了数据。它的特点是通讯速度比较快。大家都知道TCP是要经过三次握手的,而UDP没有。
基于上述不同,UDP和TCP编程步骤也有些不同,如下:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
与之对应的UDP编程步骤要简单许多,分别如下:
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接
一.TCP流式套接字的编程步骤
在使用之前须链接库函数:工程->设置->Link->输入ws2_32.lib
服务器端程序
流程
1、加载套接字库
2、创建套接字(socket)。
3、将套接字绑定到一个本地地址和端口上(bind)。
4、将套接字设为监听模式,准备接收客户请求(listen)。
5、等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
6、用返回的套接字和客户端进行通信(send/recv)。
7、返回,等待另一客户请求。
8、关闭套接字。
服务器端代码
#include <Winsock2.h>//加裁头文件
#include <stdio.h>//加载标准输入输出头文件
void main()
{
WORD wVersionRequested;//版本号
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );//1.1版本的套接字
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) { return; }//加载套接字库,加裁失败则返回
if ( LOBYTE( wsaData.wVersion ) != 1 ||HIBYTE( wsaData.wVersion ) != 1 ){
WSACleanup( );
return;
}//如果不是1.1的则退出
SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);//创建套接字(socket)。
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
//转换Unsigned short为网络字节序的格式
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//将套接字绑定到一个本地地址和端口上(bind) listen(sockSrv,5);
//将套接字设为监听模式,准备接收客户请求(listen)。
SOCKADDR_IN addrClient;//定义地址族
int len=sizeof(SOCKADDR);//初始化这个参数,这个参数必须被初始化
while(1){
SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);
//accept的第三个参数一定要有初始值。
//等待客户请求到来;当请求到来后,接受连接请求,
//返回一个新的对应于此次连接的套接字(accept)。
//此时程序在此发生阻塞
char sendBuf[100];
sprintf(sendBuf,"Welcome %s to www.msn.com.cn",inet_ntoa(addrClient.sin_addr));
//用返回的套接字和客户端进行通信(send/recv)。
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[100];
recv(sockConn,recvBuf,100,0);
printf("%s\n",recvBuf);
closesocket(sockConn);//关闭套接字。等待另一个用户请求
}
}
客户端程序
流程
1、加载套接字库
2、创建套接字(socket)。
3、向服务器发出连接请求(connect)。
4、和服务器端进行通信(send/recv)。
5、关闭套接字。
客户端代码
#include <Winsock2.h>
#include <stdio.h>
void main(){
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );//加载套接字库
if ( err != 0 ) { return; }
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ){
WSACleanup( );
return;
}
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);//创建套接字(socket)。
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//向服务器发出连接请求(connect)。
char recvBuf[100];//和服务器端进行通信(send/recv)。
recv(sockClient,recvBuf,100,0);
printf("%s\n",recvBuf);
send(sockClient,"This is blues_j",strlen("This is blues_j")+1,0);
closesocket(sockClient);//关闭套接字。
WSACleanup();//必须调用这个函数清除参数
}
二.UDP型套接字的编程步骤
服务器(接收)端程序
1、创建套接字(socket)。
2、将套接字绑定到一个本地地址和端口上(bind)。
3、等待接收数据(recvfrom)。
4、关闭套接字。
服务器端代码
#include <Winsock2.h>
#include <stdio.h>
void main(){
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) { return; }
if ( LOBYTE( wsaData.wVersion ) != 1 ||HIBYTE( wsaData.wVersion ) != 1 ){
WSACleanup( );
return;
}
SOCKET sockSrv=socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
char recvBuf[100];
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);
printf("%s\n",recvBuf);
closesocket(sockSrv); WSACleanup();
}
客户端(发送)端程序
客户端(发送端)程序:
1、创建套接字(socket)。
2、向服务器发送数据(sendto)。
3、关闭套接字。
客户端端代码
#include <Winsock2.h>
#include <stdio.h>
void main(){
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) { return; }
if ( LOBYTE( wsaData.wVersion ) != 1 ||HIBYTE( wsaData.wVersion ) != 1 ){
WSACleanup( );
return;
}
SOCKET sockClient=socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
sendto(sockClient,"Hello",strlen("Hello")+1,0, (SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
closesocket(sockClient);
WSACleanup();
}
三.聊天程序中的注意事项
聊天程序常用的是UDP式套接字。因为TCP的三步握手开销比较大。
socket通讯
网络字节序
小端法: 高位存高地址,低位存低地址。 (intel架构的存储方式)
大端法:高位存低地址,低位存高地址。(网络传输的方式)
#include <arpa/inet.h>
// 将本地转网络,转IP 转192.168.1.11->string->atoi->int->htonl->网络字节序,可以使用
int inet_pton();
进行直接转换
uint32_t htonl(uint32_t hostlong);
// 本地转网络,转port
uint16_t htons(uint16_t hostshort);
// 网络转本地,转ip
uint32_t ntohl(uint32_t netlong);
// 网络转本地,转port
uint16_t ntohs(uint16_t netshort);
// string转网络字节
int inet_pton(int af, const char * restrict src, void * restrict dst);
// af: AF_INET, AF_INET6
// src: ip地址,点分十进制
// dst: 转换之后的 网络字节序的地址
创建socket服务器的步骤
创建socket句柄
bind() 绑定ip+port
listen() 设置监听上线,同时连接数
accept() 阻塞监听客户端连接
read() 进行数据的读取,读取到的数据需要toupper()进行小写转大写
write() 写入,写给客户端返回值
当read()读到0就是close的时候
进行 close() 关闭
Socket函数分析
创建一个套接字
int socket(int domain, int type, int protocol);
domain : 所选用的ip地址协议, AF_INET, AF_INET6
type : 类型 SOCK_STREAM
(TCP/流形式), SOCK_DGRAM
(UDP/报形式)
protocol : 代表协议号 0
返回值:
成功返回0,新套接字的文件描述
失败返回: -1
#include <sys/socket.h>
fd = socket(AF_INET, SOCK_STREAM, 0)
sockaddr地址结构体分析
#include <sys/socket.h>
#include <arpa/inet.h>
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.168.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
addr.sin_addr.s_addr = htonl(INADDR_ANY)
INADDR_ANY 取出系统有效的任意ip地址,是二进制类型
bind(fd, (struct sockaddr *)&addr, size);
bind函数的分析
绑定函数
bind(fd, (struct sockaddr *)&addr, size);
fd : socket文件对象
(struct sockaddr *)&addr
: 将sockaddr存储的地址结构进行强制转换成sockaddr 进行传入
size : addr的大小,使用sizeof进行获取
listen函数分析
监听函数
int listen(int sockfd, int backlog);
sockfd : 套接字
backlog : 最大连接数,最大为128
返回值: 0, -1 error
accept函数分析
堵塞函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd : socket 函数返回值
addr : 传出参数, 成功返回服务器的ip和端口号
addrlen : 传入传出。入: addr的大小。 出:客户端addr的实际大小
返回值:
成功:大于0,返回成功的套接字文件描述符
失败:返回-1
connect函数分析
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
使用现有的socket与服务器建立连接
sockfd: socket 函数返回值
addr: 传入服务器的地址结构
返回值:
成功:0
失败:-1
小Tips(以后专门整理)
- WM_APP:
想要定义其自己消息的应用程序应该使用WM_APP。WM_APP 是确保不会与系统(WM_CREATE 等等)或类/特定控件消息如DM_GETDEFID 相冲突的。 - WSACleanup:
每次调用WSAStartup,都要调用相应的WSACleanup;因为每次启动调都会增加加载winsock DLL的引用计数,因此必须调同样多的WSACleanup,以减少引用计数 - Winsock API:
在“传输层”和“会话层”之间。 - TCP/UDP:
TCP:可靠连接。字节流,无报文边界。
UDP:无确认,不可靠,无连接。有报文边界。 - 端口:
0~1023:系统保留;
1024~65535:用户使用; - 同时打开的套接字数目:不固定的,与可用的物理内存有关。
7.一般受到SOCKET_ERROR后,除了WSAEWOULDBLOCK, 都应该关闭套接字,因为它不能再用了。 - recv可以“消息取数”,即偷看。但不好,性能下降(2次系统调用)。
- recv:把数据留在参数缓冲中不好,那样剩余缓冲量将减少,可能使系统减少发送端TCP窗口容量,从而使网络流量减少。所以最好把接收到的数据复制到自己的buffer中。
- send: TCP流情况下,发出的字节也许少于理想的字节。解决方法:用一个循环发送。
- TCP流情况下,10个send, 也许被1-2个recv就全接收了。
- UDP: 有connect, 可使用send;
无connect, 只可使用sendto - WSAEWOULDBLOCK: 很多情况下,都可能出现。
- GetLastError: 独立与线程的,线程之间各自有各自的,不影响!!!
- MailSlot: 客户机到服务器,不可靠,单向。垃圾!
- 消息长了,尽量考虑面向连接,因为无连接分片,丢一个就白折腾了