准备做一个zmq专题,先复习一下socket的知识。
socket
我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket编程的基本流程
注意阻塞的部分
下面的例子对照图来看,可以大致说明整个编程模型。
服务端采用fork同时服务多个客户
注意服务器accept以后,返回的fd是在另一个进程中使用的。
服务器
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
void sig_child(int signo);
int main(void)
{
pid_t pid;
struct sigaction act;
struct sockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
int listenfd,connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i,n,m;
memset(&act,0,sizeof(act));
act.sa_handler = sig_child;
if(sigaction(SIGCHLD,&act,0)){
perror("Sigaction Error");
return 1;
}
listenfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
Listen(listenfd,20);
printf("Accepting connections ...\n");
while(1)
{
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
m = fork();
if(m == -1)
{
perror("call to fork");
exit(1);
}
else if(m == 0)
{
while(1)
{
n = Read(connfd,buf,MAXLINE);
if(n == 0){
printf("the other side has been closed.\n");
break;
}
printf("received from %d at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
for(i=0;i<n;i++)
buf[i] = toupper(buf[i]);
//printf("content %s",buf);
Write(connfd,buf,n);
}
Close(connfd);
exit(0);
}
else
{
Close(connfd);
}
}
}
// SIGCHLD handler
void sig_child(int signo)
{
pid_t pid;
int stat;
while((pid = waitpid(-1,&stat,WNOHANG)) > 0)
{
printf("chile %d terminated\n",pid);
}
return;
}
客户端
/* File Name: client.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
exit(0);
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
perror("recv error");
exit(1);
}
buf[rec_len] = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);
}
accept函数
上面一个很重要的代码片段就是用了accept函数,而且accept函数返回了一个socket。我们有必要搞清楚这个反悔的socket是什么。
accept函数干嘛了
The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket. The newly created socket is not in the listening state. The original socket sockfd is unaffected by this call.返回了什么
RETURN VALUE
On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
由此可见accept函数用在tcp协议上,返回一个链接的socket的文件描述符,我们可以通过这个文件描述符来服务写入数据,这个数据来源于客户端,写入到远程。而服务器一开始创建的socket是专门用来监听客户端链接请求的。
所谓的accept函数,其实抽象的是TCP的连接建立过程。accept函数返回的新socket其实指代的是本次创建的连接,而一个连接是包括两部分信息的,一个是源IP和源端口,另一个宿IP和宿端口。每次accept返回的这些socket宿端口就可以都是一样的,这里并没有重新监听一个端口,端口主要用来区分进程,而源IP和源端口随着客户端不同显然不同。
总结
上面的图和代码都是网上copy的,服务器客户端不是配对的,代码也不能编译。只是找了两个觉得写得还比较清晰的。
主要目的是为了说明socket编程的基本模型,用来复习的。下面的文章说明zmq的时候,会进行比较,zmq的socket和这里的socket是不同的概念,zmq的socket会抽象出更高级的概念,zmq的socket会摆脱这里socket和socket一一对应通信的限制,大大简化网络通信方式,使我们能开发出很多高级的功能。