Socket编程的基础和基本过程
一、什么是socket
接触网络编程当然要了解socket,socket就是套接字,它的定义是一种软件抽象,用于表达两台机器之间的连接…终端?。对于一个给定的连接,每台机器上都有一个套接字。有点像我们打电话,用电缆把电话连接起来,中间的物理结构和具体实现过程我们不必了解,只要我们能进行通话即可。当我们进行编程时,需要用到socket接口,socket接口定义了许多函数或例程,我们可以直接调用它们实现网络连接、通信等功能。socket接口为我们建立通信信道,我们可以通过这条通道来与一台或多台计算机进行连接。
二、socket编程的基本过程,
下面我们介绍的都是以基本的Windows socket API为例:
1.建立socket:
socket编程,其最基本的步骤就是建立socket,下面来看看建立socket的函数原型:
SOCKET socket(int af,int type,int protocol );
第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置PF_INET;第二个参数指定要创建的套接字类型,一般分为流套接字和数据报套接字两种类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM;第三个参数指定应用程序所使用的通信协议。该函数如果调用成功就返回新创建的套接字的描述符,如果失败则返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
2.socket的配置:
通过socket函数调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket。面向客户端的socket通过调用connect函数在socket数据结构中保存本地和远端信息。无连接客户端、服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。bind函数将socket与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。下面看看connect函数和bind函数:
int bind(SOCKET s, const struct sockaddr FAR *name, int namelen );
当创建了一个socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。
一个服务程序必须调用bind函数来给其绑定一个IP地址和一个特定的端口号。客户程
序一般不必调用bind函数来为其socket绑定IP地址和断口号。该函数的第一个参数指
定待绑定的socket描述符;第二个参数指定一个sockaddr结构,该结构是这样定义的:
struct sockaddr
{ unsigned short sa_family;
char sa_data[14];
};
sa_family指定地址族,对于TCP/IP协议族的套接字,给其置AF_INET。当对TCP/IP协议族的套接字进行绑定时,我们通常使用另一个地址结构:
struct sockaddr_in
{ short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中sin_family置AF_INET;sin_port指明端口号;sin_addr结构体中只有一个唯一的字段s_addr,表示IP地址,该字段是一个整数,一般用函数inet_addr把字符串形式的IP地址转换成unsigned long型的整数值后再置给s_addr。有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其socket绑定IP地址时可以把htonl(INADDR_ANY)置给s_addr,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的服务程序的socket绑定一个固定的IP地址,那么就只有与该IP地址处于同一个网段上的客户程序才能与该服务程序通信。
我们用0来填充sin_zero数组,目的是让sockaddr_in结构的大小与sockaddr结构的大小一致。
3. 实现功能
①服务器端:需要对绑定的端口进行监听,函数原型如下:
int listen( SOCKET s, int backlog );
服务程序可以调用listen函数使其流套接字s处于监听状态。Backlog为客户连接
请求队列的最大数量。处于监听状态的流套接字s将维护一个客户连接请求队列。
执行成功,则返回0;如果执行失败,则返回SOCKET_ERROR。
此外,还需要从请求队列中取出最前一个客户请求,需要用到accept()函数:
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回INVALID_SOCKET。该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。
②客户端:
connect函数:int connect(SOCKET s,const struct sockaddr FAR* name,int
namelen);
connect函数是为了与远程主机建立直接连接而使用的。该函数的参数是相当清楚
的:s是即将在其上建立见解的那个有效的TCP套接字;name是针对TCP(说明连接
的服务器)的套接字的结构;namelen则是名字参数的长度。
send函数:int send(SOCKET s, const char FAR *buf, int len,int flags );
该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程
序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;第四个参
数一般置0。这里只描述同步Socket的send函数的执行流程。当调用该函数时,send
先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发
送缓冲区的长度,该函数返回SOCKET_ERROR;如果len小于或者等于s的发送缓冲
区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等
待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发
送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len,如果len
大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果len
小于剩余空间大小send就仅仅把buf中的数据复制到剩余空间里。如果send函数复
制数据成功,就返回实际复制的字节数,如果send在复制数据时出现错误,那么
send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么
send函数也返回SOCKET_ERROR。要注意send函数把buf中的数据成功复制到s的发
送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接
的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket
函数就会返回SOCKET_ERROR。(每一个除send外的socket函数在执行的最开始总
要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出
现网络错误,那么该socket函数就返回SOCKET_ERROR)。
注意:send函数客户端和服务器端应用程序都可以用send函数来向TCP连接的另一端发
送数据。一般客户端发送请求,服务器端发送应答。
下面还有一个服务器端和客户端都需要的函数recv();
int recv(SOCKET s, char FAR *buf, int len, int flags );
该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该
缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数
一般置0。函数调用成功返回实际接收的字节数,如果接收出错则返回
SOCKET_ERROR,接受数据时网络中断返回0。
4、关闭socket
当完成服务或者某任务时,记得请把socket关闭:int closesocket (SOCKET s);
三、小结及例子分析
1、服务器端的工作流程是这样的:首先调用socket函数建立socket,然后调用bind 函
数将socket与本机IP地址和端口进行绑定,接着调用listen对socket进行监听,当调用accpet函数接收到一个连接服务请求时,便生成一个新的socket。服务器显示该客户机的IP地址,并通过新的socket向客户端发送字符串"Hello!"。最后关闭该socket。服务器端重要部分程序如下:
main()
{
int sockfd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */ struct sockaddr_in my_addr; /* 本机地址信息 */
struct sockaddr_in remote_addr; /* 客户端地址信息 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {/*创建socket*/
printf("socket创建出错!"); exit(1);
}
my_addr.sin_family=AF_INET;/*协议族*/
my_addr.sin_port=htons(SERVPORT);/*端口设定*/
my_addr.sin_addr.s_addr = INADDR_ANY;/*设置的IP地址信息*/
memset(&(my_addr.sin_zero),0,sizeof(my_addr.sin_zero));
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) /*绑定此socket*/
== -1) {
printf("bind出错!");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {/*对socket进行监听*/
printf("listen出错!");
exit(1);
}
while(1) {
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr,
&sin_size)) == -1) {
printf("accept出错");
continue;
}
printf("received a connection from %sn", inet_ntoa(remote_addr.sin_addr));
if (send(client_fd, "Hello!", 6, 0) == -1)
printf("send出错!");
close(client_fd);
exit(0);
close(client_fd);
}
}
}
2、客户端程序流程是这样的:在建立socket以后用connect函数与服务器端连接,连接成
功以后用recv函数接收服务器端发来的消息。客户端程序重要部分代码如下:
#define SERVPORT 3333
#define MAXDATASIZE 100 /*每次最大数据传输量 */
main(int argc, char *argv[]){
int sockfd, recvbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if (argc < 2) {
printf("Please enter the server's hostname!\n");
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) {
printf("gethostbyname出错!");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
printf("socket创建出错!");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
memset(&(my_addr.sin_zero),0,sizeof(my_addr.sin_zero));
if (connect(sockfd, (struct sockaddr *)&serv_addr, /*对服务器端进行连接*/
sizeof(struct sockaddr)) == -1) {
printf("connect出错!");
exit(1);
}
if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {/*从服务器端接收信息*/
printf("recv出错!");
exit(1);
}
printf(“%s”,buf);
}