套接字编程基础
一种双向的通信端口。位于网络中的主机通过连接的套接字提供的接口进行数据传输。
套接字与端口
套接字是一种使用标准UNIX文件描述符(file descriptor)与其他程序通信的方式。套接字可以看作是处于不同主机之间的两个程序的通信连接端点。一方面程序将要传输的信息写入套接字中,而另一方面则通过读取套接字内的数据来获得传输的信息。
![]()
图所示为使用套接字进行通信的示意图。假设存在两台主机A与B,在主机A中存在进程C,主机B中存在进程D,当进程C需要将数据送到进程D时,首先将数据写到套接字中,而进程D可以通过读取套接字来获得进程C发送的信息。
在网络中,不同计算机是通过IP地址来区分的,也就是说,要将数据由主机A发送到主机B,只要知道主机B的IP地址就可以确定数据要发送的目的地。但是,在主机A与B中不可能只有进程C和进程D两个进程。主机B在收到主机A发送来的数据后,如何才能确定该数据是发送给进程D?因此,还需要某种标识信息,用于描述网络通信数据发往的进程。TCP/IP协议提出了协议端口的概念,用于标识通信的进程。
当进程与某个端口绑定后,操作系统会将收到的给该端口的数据送往该进程。与文件描述符类似,每个端口都有被称为端口号的整数类型的标识符,该标识符用于区分不同的端口。不同协议可以使用相同的端口号进行数据传输。例如,TCP使用了344的端口号,UDP同样可以使用344端口号进行数据传输。
端口号为一个16位的无符号整数,其取值范围为0~65535。低于256的端口被作为系统的保留端口号,主要用于系统进程的通信,不在这一范围的端口号被称为自由端口号,可以由进程自由使用。
一 3种类型的套接字:
1.流式套接字(SOCKET_STREAM)
提供面向连接的可靠的数据传输服务。数据被看作是字节流,无长度限制。例如FTP协议就采用这种。
2.数据报式套接字(SOCKET_DGRAM)
提供无连接的数据传输服务,不保证可靠性。
3.原始式套接字(SOCKET_RAW)
该接口允许对较低层次协议,如IP,ICMP直接访问。
二 基本套接字系统调有有如下一些:
创建套接字: socket()
绑定本机端口: bind()
建立连接: connect(),accept()
侦听端口: listen()
数据传输: send(), recv()
输入/输出多路复用: select()
关闭套接只: closesocket()
三 数据类型
struct sockaddr
{
unsigned short sa_family; //地址族, 一般为AF_INET
char sa_data[14]; //14字节的协议地址
}
struct sockaddr_in
{
short int sin_family; //地址族
unsigned short int sin_port; //端口号
struct in_addr in_addr; //ip地址
unsigned char sin_zero[8]; //填充
}
服务器: server_addr.sin_addr = ADDR_ANY, 那么当 bind 调用的时候, 系统会accept 所有连接本机地址的客户端的连接请求.
客户端: connect调用时, 如果没有 bind, 系统则会自动分配一个本机 ip(如果你有双网卡) 和端口号 port 给当前连接. 如果需要调用 bind 明确绑定ip (如果不这么做, 服务器程序可能过滤掉一些非法ip), 但是我们又不知道那些可用port, 这时, 我们可以设置client_addr.sin_port = 0, 这样, 系统就在 bind client 和client_addr时, 自动分配一个可用的port, 而不用我们自己操心去找可用端口了.
四 常用函数:
1 socket()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol)
domain: 协议类型,一般为AF_INET
type: socket类型
protocol:用来指定socket所使用的传输协议编号,通常设为0即可
返回一个套接口描述符,如果出错,则返回-1。
一旦你有了一个套接口以后,下一步就是把套接口绑定到本地计算机的某一个端口上。但如果你只想使用connect()则无此必要。
2 bind()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd: socket描述符
my_addr:是一个指向包含有本机ip地址和端口号等信息的sockaddr类型的指针
addrlen:常被设为sizeof(struct sockaddr)
如果出错,bind()也返回-1。
![]()
Code
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#define MYPORT 3490
main()
{
int sockfd;
struct sockaddr_inmy_addr;
sockfd=socket(AF_INET,SOCK_STREAM,0);/*do someerror checking!*/
my_addr.sin_family=AF_INET;/*hostbyteorder*/
my_addr.sin_port=htons(MYPORT);/*short,network byte order*/
my_addr.sin_addr.s_addr=inet_addr("132.241.5.10");
bzero(&(my_addr.sin_zero),8);/*zero the rest of the struct*/
/*don't forget your error checking for bind():*/
bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr));
![]()
如果你使用connect()系统调用,那么你不必知道你使用的端口号。当你调用connect()时,它检查套接口是否已经绑定,如果没有,它将会分配一个空闲的端口。
3 connect()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
sockfd: 目的服务器的socket描述符
serv_addr:包含目的机器ip地址和端口号的指针
addrlen:sizeof(struct sockaddr)
如果出错,返回-1。
![]()
Code
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#define DEST_IP "132.241.5.10"
#define DEST_PORT 23
main()
{
intsockfd;
structsockaddr_indest_addr;/*will hold the destination addr*/
sockfd=socket(AF_INET,SOCK_STREAM,0);/*do some error checking!*/
dest_addr.sin_family=AF_INET;/*hostbyteorder*/
dest_addr.sin_port=htons(DEST_PORT);/*short,network byte order*/
dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero),8);/*zero the rest of the struct*/
/*don'tforgettoerrorchecktheconnect()!*/
connect(sockfd,(structsockaddr*)&dest_addr,sizeof(struct sockaddr));
![]()
如果你希望不连接到远程的主机,也就是说你希望等待一个进入的连接请求,然后再处理它们。这样,你通过首先调用listen(),然后再调用accept()来实现。
4 listen()
头文件:
#include <sys/socket.h>
函数原型:
int listen(int sockfd, int backlog);
sockfd:socket()系统调用返回的socket描述符
backlog:指定在请求队列中的最大请求数,进入的连接请求将在队列中等待accept()它们。
在远程的主机可能试图使用connect()连接你使用listen()正在监听的端口。但此连接将会在队列中等待,直到使用accept()处理它。调用accept()之后,将会返回一个全新的套接口文件描述符来处理这个单个的连接。这样,对于同一个连接来说,你就有了两个文件描述符。原先的一个文件描述符正在监听你指定的端口,新的文件描述符可以用来调用send()和recv()。
5 accept()
头文件:
#include <sys/types.h>
#inlcude <sys/socket.h>
函数原型:
int accept(int sockfd, void *addr, int addrlen)
sockfd:是被监听的socket描述符
addr:通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息
addrlen:sizeof(struct sockaddr_in)
![]()
Code
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#define MYPORT 3490/*theportuserswillbeconnectingto*/
#define BACKLOG 10/*howmanypendingconnectionsqueuewillhold*/
main()
{
intsockfd,new_fd;/*listenonsock_fd,newconnectiononnew_fd*/
structsockaddr_inmy_addr;/*myaddressinformation*/
structsockaddr_intheir_addr;/*connector'saddressinformation*/
intsin_size;
sockfd=socket(AF_INET,SOCK_STREAM,0);/*dosomeerrorchecking!*/
my_addr.sin_family=AF_INET;/*hostbyteorder*/
my_addr.sin_port=htons(MYPORT);/*short,networkbyteorder*/
my_addr.sin_addr.s_addr=INADDR_ANY;/*auto-fillwithmyIP*/
bzero(&(my_addr.sin_zero),8);/*zerotherestofthestruct*/
/*don'tforgetyourerrorcheckingforthesecalls:*/
bind(sockfd,(structsockaddr*)&my_addr,sizeof(structsockaddr));
listen(sockfd,BACKLOG);
sin_size=sizeof(structsockaddr_in);
new_fd=accept(sockfd,&their_addr,&sin_size);
![]()
6 send()
头文件:
#include <sys/socket.h>
函数原型:
int send(int sockfd, const void *msg, int len, int flags);
sockfd:用来传输数据的socket描述符
msg:要发送数据的指针
flags: 0
系统调用send()返回实际发送的字节数,这可能比你实际想要发送的字节数少。如果返回的字节数比要发送的字节数少,你在以后必须发送剩下的数据。当send()出错时,将返回-1。
7 recv()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int recv(int sockfd, void *buf, int len, unsigned int flags)
sockfd:接收数据的socket描述符
buf:存放数据的缓冲区
len:缓冲的长度
flags:0
8 sendto()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
除了两个参数以外,其他的参数和系统调用send()时相同。
参数to是指向包含目的IP地址和端口号的数据结构sockaddr的指针。
参数tolen可以设置为sizeof(structsockaddr)。
系统调用sendto()返回实际发送的字节数,如果出错则返回-1。
9 recvfrom()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int fromlen)
参数from是指向本地计算机中包含源IP地址和端口号的数据结构sockaddr的指针。
参数fromlen设置为sizeof(struct sockaddr)。
系统调用recvfrom()返回接收到的字节数,如果出错则返回-1。
10 read() write()
int read(int fd, char *buf, int len)
int write(int fd, char *buf, int len)
11 shutdown()
close(sockfd)
int shutdown(int sockfd, int how)
第一个参数是你希望切断通信的套接口文件描述符。第二个参数how值如下:
0—Furtherreceivesaredisallowed
1—Furthersendsaredisallowed
2—Furthersendsandreceivesaredisallowed(likeclose())
getpeername()
这个系统的调用十分简单。它将告诉你是谁在连接的另一端:
#include<sys/socket.h>
int getpeername(int sockfd,struct sockaddr* addr,int* addrlen);
第一个参数是连接的数据流套接口文件描述符。
第二个参数是指向包含另一端的信息的数据结构sockaddr的指针。
第三个参数可以设置为sizeof(structsockaddr)。
如果出错,系统调用将返回-1。
一旦你获得了它们的地址,你可以使用inet_ntoa()或者gethostbyaddr()来得到更多的信息。
gethostname()
系统调用gethostname()比系统调用getpeername()还简单。它返回程序正在运行的计算机的名字。系统调用gethostbyname()可以使用这个名字来决定你的机器的IP地址。
下面是一个例子:
#include<unistd.h>
int gethostname(char*hostname,size_tsize);
如果成功,gethostname将返回0。如果失败,它将返回-1。
OSI TCP/IP模型
应用层 应用层
表示层
会话层
传输层 传输层
网络层 网络层
数据链路层 网络接口
物理层
相同层次之间不可以直接通信,是虚拟通信。下层向上层提供服务,实际通信在最底层完成。各层之
间单向依赖。
应用层:Telnet、FTP、HTTP、DNS、SMTP、POP3。
传输层:TCP实现数据的完整性,例如下载安装包;UDP实现数据的及时性,例如网络视频电话。
TCP和UDP可以使用同样的端口,1024以下的端口保留给预定义的服务。windows socket只支持
AF_INET网际域.
1、基于TCP的socket编程
服务器端程序:
创建套接字;
将套接字绑定到一个本地地址和端口上bind
将套接字设为监听模式,准备接受客户请求listen
等待客户请求,当请求到来,返回一个新的对应于此次连接的套接字accept
用返回的套接字和客户端进行通信send/recv。
返回,等待另一客户的请求。
关闭套接字。
客户端程序:
创建套接字;
向服务器发送连接请求connect
和服务器进行通信send/recv
关闭套接字。
2、基于UDP的socket编程
服务器端(接收端)程序:
创建套接字
绑定套接字到本地地址和端口上bind
等待接受数据recvfrom
关闭套接字
客户端(发送端)程序:
创建套接字
发送数据sendto
关闭套接字
127.0.0.1是回路地址。可以在同一台机子上通信。
基于TCP/IP的socket代码:
![]()
Code
/*Server.cpp*/
#include<winsock2.h>
#include<stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
//加载套接字DLL库
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 2 ||
HIBYTE( wsaData.wVersion ) != 2 )
{
//终止使用套接字DLL库
WSACleanup( );
return;
}
//1、创建套接字
SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);
/*第一个参数对于TCP/IP协议的套接字,只能是AF_INET或PF_INET,第二个参数对于
1.1版本的socket,只支持两种类型的套接字,SOCK_STREAM指定产生流式套接字,
SOCK_DGRAM产生数据报套接字,第三个参数为零时自动选择合适的协议。*/
//2、绑定套接字到一个本地地址和端口上
SOCKADDR_IN addSrv;
//htonl是把u_long类型转换为网络字节序
//htons是把u_short类型转换为网络字节序
addSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addSrv.sin_family=AF_INET;
//unsigned short sin_port;
addSrv.sin_port=htons(4000);
bind(sockSrv,(SOCKADDR*)&addSrv,sizeof(SOCKADDR));
//3、将套接字设为监听模式,准备接受客户请求
listen(sockSrv,5);
SOCKADDR_IN addClient;
int len=sizeof(SOCKADDR);
while(1)
{
SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addClient,&len);
//accept函数第三个参数必须赋初始值,初始值为结构体的长度。否则函数调用失败
//4、用返回的套接字和客户端进行通信
char sendBuf[100];
sprintf(sendBuf,"hello,haha %s",inet_ntoa(addClient.sin_addr));
//inet_ntoa函数是将网络IP地址转换为字符串
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[100];
recv(sockConn,recvBuf,100,0);
printf("%s\n",recvBuf);
//4、关闭套接字
closesocket(sockConn);
}
}
/*Client.cpp*/
#include <stdio.h>
#include <Winsock2.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
//1、创建套接字
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);
//2、向服务器发送连接请求
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//设置SOCKET的IP为本机。
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(4000);//端口为4000,服务端和客户端的端口一定要相同.否则是连不
上的
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//根据addrSrv连接服务端
//3、和服务器进行通信
char recvBuf[100];
recv(sockClient,recvBuf,100,0);//接收100字符
printf("%s\n",recvBuf); //显示
send(sockClient,"hello",strlen("hello")+1,0); //发送一个hello
//4、关闭套接字
closesocket(sockClient); //关闭
WSACleanup(); //清理
}
13、基于UDP/IP的socket代码:
/*Server.cpp*/
#include<winsock2.h>
#include<stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
//加载套接字DLL库
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 2 ||
HIBYTE( wsaData.wVersion ) != 2 )
{
//终止使用套接字DLL库
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(4001);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
char recvBuf[50];
recvfrom(sockSrv,recvBuf,50,0,(SOCKADDR*)&addrClient,&len);
printf("%s\n",recvBuf);
closesocket(sockSrv);
WSACleanup();
}
/*Client.cpp*/
#include<winsock2.h>
#include<stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
//加载套接字DLL库
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 2 ||
HIBYTE( wsaData.wVersion ) != 2 )
{
//终止使用套接字DLL库
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(4001);
sendto(sockClient,"hello",strlen("hello")+1,0,
(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
closesocket(sockClient);
WSACleanup();
}
QQ一般都是8080端口或者4000端口