本篇博客将介绍基于IPv4的socket网络编程。
一、socket
1.概念:
socket这个词可以表示很多概念:在TCP/IP协议中,“IP地址 + TCP或UDP端口号”唯一标识网络通信中的一个进程,“IP地址 + 端口号”就叫做socket。 在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个一个连接,socket本身就有“插座”的意思,因此用来描述网络的一对一关系。
2.socket相关函数
1. socket函数
我们使用系统调用 socket函数是用来获得文件描述符 原型:
#include < sys/types.h>
#include< sys/socket.h>
int socket(int domain, int type, int protocol);第一个参数domain表示,设置为“AF_INET”。
第二个参数为套接口的类型:SOCK_ STREAM或SOCK_DGRAM。
第三个设置为 0。
2. bind函数
我们在编写服务器的时候,一旦创建套接字,就用bind函数将套接字和本地计算机的一个端口绑定。原型:
#include< sys/types.h>
#include< sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);第一个参数就是socket函数返回的套接字文件描述符。
第二个参数addr是指向数据结构sockaddr的指针。数据结构sockaddr中包含了关于本机地址,端口号和IP地址的信息。
第三个参数 addrlen表示struct sockaddr结构体的大小,可以设置成sizeof(struct sockaddr)。
3. connect函数
这个函数在客户端的程序中使用,用来连接所需要访问的服务器。
# include< sys/types.h>
# include< sys/socket.h>
int connect(int sockfd,struct sockaddr* serv_ addr,int addrlen);
第一个参数是套接字文件描述符,由socket函数返回。
第三个参数表示sockaddr结构体大小。使用sizeof(struct sockaddr)得到。
4. listen函数
用来监听自己的端口,如果有主机访问这个端口,那就把此主机信息存储在一个队列中等待。原型:
# inlcude < sys/types.h>
#include < sys/socket.h>
int listen(int sockfd,int backlog);
第一个参数是socket函数返回的套接字文件描述符。
第二参数是进入队列中允许连接的个数。这个值是队列中最多可以请求的个数。大多数系统设置为缺省值20,我们可以设置成10左右。
5. accept函数
在上面的函数中我们说监听到的主机信息存放在一个队列中,而这个accept函数就是将队列中的信息拿出来处理。在调用accept函数后,就返回一个全新的套接字文件描述符来处理这单个连接。这样,对于同一个连接来说,就有了两个文件描述符。原来的文件描述符还是去监听之的端口,新的文件描述符用来读写数据。下面看一下函数
#include < sys/socket.h>
int accept( int sockfd, struct sockaddr* addr, socklen_ t addrlen)
第一个参数是正在监听端口的套接字文件描述符。
第二个参数是个sockaddr(sockaddr_in)结构体指针,是个输入型参数, 就是将所访问主机的IP地址和端口存到这个结构体里面。通过他可以了解哪个主机在哪个端口呼叫你。
第三个参数使用sizeof(sockaddr_in)来获得。
6. 在编写服务器的过程中我们需要用到以下函数:
#include <arpa/inet.h>
uint32 _ t htonl(uint32_t hostlong);
uint16_ t htons(uint16_t hostshort);
uint32_ t ntohl(uint32_t netlong);
uint16_ t ntohs(uint16_t netshort);
这些库函数用来做网络字节序和主机字节序的转换。这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果 主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些数不做转换,将参数原封不动地返回。还有一个函数:
in_ addr_ t inet_addr(const char *cp);
将点分十进制转换为网络字节序。
二、接下来我们就用上面的函数来实现客户端/服务器程序。
先来说说实现一个服务器的流程吧。实现一个服务器需要以下几步:
① 首先要知道本机的IP地址,在linux下可用ifconfig命令来查询IP地址。我们还要指定一个端口,将此端口与服务器绑定。(假设程序中指定的端口为8080);
② 调用socket()函数,为进程分配一个套接字文件描述符。
③ 调用bind()函数将进程和指定端口绑定。(因为别的主机访问的是我的主机的端口,而此时该端口与当前进程所绑定,所以当前进程可以通过此端口与别的主机进行通信。)
④ 调用listen()函数进行监视该端口。当有主机访问该端口时,listen()会将主机信息放入等待队列中排队。
⑤ 调用accept()函数,从等待队列中将访问的信息拿出来处理,为其分配一个专用的文件描述符。用此描述符来进行两台主机之间的通信。
⑥ 开始通信(read,write)
实现客户端的流程:
① 调用socket()函数,为进程分配一个套接字文件描述符。
② 调用connect函数连接想要访问的服务器。
③ 与服务器连接完成,开始传输数据。
1.单进程服务器
tcp_server.c(服务器)
#include<stdlib.h>
#include<sys/types.h>
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
void usage(char* s)
{
printf("Please input:%s [in_addr] [port_addr]\n",s);
//例如:./tcp_server 192.168.43.94 8080
(192.168.43.94是本机IP地址,8080是想要指定的端口)
}
int main(int argc, char* argv[])
{
if(argc != 3)//运行服务器要按指定格式输入
{
usage(argv[0]);
exit(1);
}
//1. 调用socket函数获取一个文件描述符
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
exit(2);
}
//printf("%d\n",sock);
//2. 调用bind函数将此进程与所指定的端口(8080)绑定
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
bzero(&server_socket, sizeof(server_socket));
server_socket.sin_family = AF_INET;
server_socket.sin_addr.s_addr = inet_addr(argv[1]);
server_socket.sin_port = htons(atoi(argv[2]));
int bind_num = bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr));
if(bind_num < 0)
{
close(sock);
perror("bind");
exit(3);
}
//3. 用listen函数对端口进行监听
if(listen(sock, 10) < 0)
{
close(sock);
perror("listen");
exit(4);
}
//4 调用accept函数开始处理信息
while(1)
{
int len = 0;
int new_fd = accept(sock, (struct sockaddr*)&client_socket, &len);//第二个参数是个输入型参数,是为了获得客户端的ip地址及端口号。
if(new_fd < 0)
{
continue;
}
char buf[1024];
memset(buf, '\0',sizeof(buf));
inet_ntop(AF_INET, &client_socket.sin_addr, buf, sizeof(buf));//将客户端的ip地址转化成点分十进制
printf("get client ip :%s\n",buf);//打印客户端ip
while(1)//这个循环就是开始收发数据
{
char buff[1024];
memset(buff, '\0', sizeof(buff));
printf("client :# ");
fflush(stdout);
ssize_t s1 = read(new_fd, buff, sizeof(buff)-1);
if( s1 > 0)
{
printf("%s\n", buff);
printf("server :# ");
printf("%s\n", buff);
ssize_t s2 = write(new_fd, buff, strlen(buff));
if(s2 < 0)
{
perror("write");
}
}
else if(s1 == 0)
{
printf("client close!\n");
return 5;
}
else
{
perror("read");
return 6;
}
}
}
close(sock);
return 0;
}
tcp_client.c(客户端)
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<string.h>
#include<netinet/in.h>
void usger(char* s)
{
printf("Please input:%s [in_addr] [port_addr]\n",s);
}
int main(int argc, char* argv[])
{
printf("1\n");
if(argc != 3)
{
usger(argv[0]);
exit(1);
}
//printf("client\n");
//1.获得文件描述符
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_sock;
server_sock.sin_family = AF_INET;
server_sock.sin_port = htons(atoi(argv[2]));
server_sock.sin_addr.s_addr = inet_addr(argv[1]);
//2.连接服务器,此时调用connect相当于三次握手的第一次握手
int ret = connect(sock, (struct sockaddr*)&server_sock, sizeof(server_sock));
if(ret < 0)
{
perror("correct");
return 1;
}
//发收数据
while(1)
{
printf("client :# ");
fflush(stdout);
char buf[1024];
int count = read(0, buf, sizeof(buf)-1);
buf[count] = '\0';
write(sock, buf, strlen(buf));
read(sock, buf, sizeof(buf)-1);
printf("server :# %s\n",buf);
}
close(sock);
return 0;
}
运行结果:
好了,大概就上面这个样子了0.0。
2.多进程服务器
如果理解了单进程的服务器,那么多进程服务器也就非常简单了,我们只需将通信部分让子进程去处理就好了,其他过程让父进程来处理。但是问题来了,只要服务器一直运行,我们的父进程就不会结束,而子进程随时可能结束,这样一来子进程就变成了僵尸进程,那么这个问题怎么处理呢?其实处理这个问题非常巧妙,我们让子进程再fork一次,得到一个孙子进程,然后结束子进程,这样孙子进程就成了孤儿进程,它会被1号进程回收,在这个过程中并没有产生僵尸进程,这个问题就解决了。我们看看代码中的实现。
#include<stdlib.h>
#include<sys/types.h>
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
void usger(char* s)
{
printf("Please input:%s [in_addr] [port_addr]\n",s);
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usger(argv[0]);
exit(1);
}
//printf("server\n");
//1 get socket fd
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
exit(2);
}
printf("%d\n",sock);
//2 bind fd for port
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
bzero(&server_socket, sizeof(server_socket));
server_socket.sin_family = AF_INET;
server_socket.sin_addr.s_addr = inet_addr(argv[1]);
server_socket.sin_port = htons(atoi(argv[2]));
int bind_num = bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr));
if(bind_num < 0)
{
close(sock);
perror("bind");
exit(3);
}
//3 listen
if(listen(sock, 10) < 0)
{
close(sock);
perror("listen");
exit(4);
}
//4 accept
while(1)
{
int len = 0;
int new_fd = accept(sock, (struct sockaddr*)&client_socket, &len);
if(new_fd < 0)
{
continue;
}
char buf[1024];
memset(buf, '\0',sizeof(buf));
inet_ntop(AF_INET, &client_socket.sin_addr, buf, sizeof(buf));
printf("get client ip :%s\n",buf);
pid_t id = fork();//第一次fork
if (id < 0)
{
perror("perror");
}
else if (id == 0)
{
close(sock);//关闭不用的文件描述符
pid_t id = fork();//第二此fork
if (id < 0)
{
perror("fork");
exit(5);
}
else if(id == 0)//让孙子进程去处理
{
while(1)
{
char buff[1024];
memset(buff, '\0', sizeof(buff));
ssize_t s = read(new_fd, buff, sizeof(buff)-1);
if(s > 0)
{
printf("client :# ");
fflush(stdout);
printf("%s\n", buff);
printf("server :# ");
printf("%s\n", buff);
write(new_fd, buff, strlen(buff)+1);
}
else if(s == 0)
{
printf("client close!\n");
break;
}
}
}
else
{
exit(6);//子进程直接退出
}
}
else
{
close(new_fd);//关闭不用的文件描述符
waitpid( id, NULL, 0);//等待子进程退出(子进程会立即退出)
}
}
close(sock);
return 0;
}
3.多线程服务器
多线程服务器和多进程服务器都差不多了,多线程服务器就是将通信部分让一个线程去处理,为了避免线程退出时整个进程退出,我们将线程分离出去。
#include<stdlib.h>
#include<sys/types.h>
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
void usger(char* s)
{
printf("Please input:%s [in_addr] [port_addr]\n",s);
}
//线程处理函数
void request(void* arg)
{
int new_fd = (int )arg;
while(1)
{
char buff[1024];
memset(buff, '\0', sizeof(buff));
ssize_t s = read(new_fd, buff, sizeof(buff)-1);
if(s > 0)
{
printf("client :# ");
fflush(stdout);
printf("%s\n", buff);
printf("server :# ");
printf("%s\n", buff);
write(new_fd, buff, strlen(buff)+1);
}
else
{
printf("client close!\n");
break;
}
}
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usger(argv[0]);
exit(1);
}
//printf("server\n");
//1 get socket fd
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
exit(2);
}
printf("%d\n",sock);
//2 bind fd for port
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
bzero(&server_socket, sizeof(server_socket));
server_socket.sin_family = AF_INET;
server_socket.sin_addr.s_addr = inet_addr(argv[1]);
server_socket.sin_port = htons(atoi(argv[2]));
int bind_num = bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr));
if(bind_num < 0)
{
close(sock);
perror("bind");
exit(3);
}
//3 listen
if(listen(sock, 10) < 0)
{
close(sock);
perror("listen");
exit(4);
}
//4 accept
while(1)
{
int len = 0;
int new_fd = accept(sock, (struct sockaddr*)&client_socket, &len);
if(new_fd < 0)
{
continue;
}
char buf[1024];
memset(buf, '\0',sizeof(buf));
inet_ntop(AF_INET, &client_socket.sin_addr, buf, sizeof(buf));
printf("get client ip :%s\n",buf);
pthread_t id;
pthread_create(&id, NULL, request, (void*)new_fd );//创建线程
pthread_detach(id);//分离线程
}
close(sock);
return 0;
}