在进行socket网络编程时, 我们需要了解一些必备的知识,例如什么是socket,ipv4的地址结构,套接字类型等等,不然上来直接看代码就会晕,当初学习网络编程时,看书上的例子,总有感觉书上讲的都很简要。再或者讲的原理太多把人绕晕。我这里只想让大家简单知道怎么使用socket进行网络编程并且给出的例子可以直接使用参考。
1. 什么是socket
(1) socket 可以看成是用户进程与网络协议栈的编程接口。就是说应用层可以看成是用户进程,传输层网络层数据链路层看成网络协议栈,因为这三个层的传输协议TCP,ip等都是已经实现好了的,那么socket就是连接这两个进行数据传递。
(2) socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程通信。
2. IPv4套接口地址结构
(1) IPv4套接口地址结构通常也称为“网际套接字地址结构”,他以socket_in命名,定义在头文件<netinet/in.h>中
struct sockaddr_in{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_len: 整个sockaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family.
sin_family: 指定该地址家族,在这里必须设为AF_INET(这里说明使用的协议是IPv4)
sin_port: 端口
sin_addr: IPv4的地址
sin_zero: 一般将其设置为0
3. 通用的地址结构
因为套接字不仅仅用于tcp/ip协议编程,所以必须有一个通用的地址结构
(1) 通用的地址结构用来指定与套接字关联的地址
struct sockaddr{
uint8_t
sin_len;
sa_family_t sin_family;
char sa_data[14];
};
sin_len: 整个sockaddr结构体的长度
sin_family: 指定该地址家族
sa_data: 由sin_family 决定它的形式
4. 网络字节序
(1) 字节序
1) 大端字节序
最高有效位存储在最低内存地址处,最低有效位存储于最高内存地址处。
2)小端字节序
最高有效位存储在最高内存地址处,最低有效位存储于最低内存地址处。
(2) 主机字节序
不同的主机有不同的字节序,如x86为小端字节序,motorola 6800为大端字节序,ARM字节序是可配置的。
(3) 网络字节序
网络字节序规定为大端字节序
5. 字节序转换函数
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,s代表short,l代表long。
6. 地址转换函数
#include<netinet/in.h>
#include<arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
int_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
7. 套接字类型
(1) 流式套接字(SOCK_STREAM)
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
(2) 数据报式套接字(SOCK_DGRAM)
提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。
(3)原始套接字(SOCK_RAW)
8. 简单的点对点回射聊天程序
server端:
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m)\
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
int listenfd;
listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
ERR_EXIT("socket");
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5788);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int bindval = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr);
if(bindval < 0)
ERR_EXIT("bind");
if((listen(listenfd, SOMAXCONN)) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn < 0)
ERR_EXIT("accept");
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int recvlen = read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, recvlen);
}
close(listenfd);
return 0;
}
client端:
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m)\
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
int sockfd;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
ERR_EXIT("socket");
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5788);
seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int conn = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(conn < 0)
ERR_EXIT("connect");
char recvbuf[1024];
char sendbuf[1024];
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sockfd, sendbuf, sizeof(sendbuf));
read(sockfd, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
return 0;
}
这里有个问题,是当server关闭之后,在登录时,登不上,会提示bind: Address already in use,地址绑定不上。
因为这时候端口被占用没有被释放,我们可以用命令:netstat -an | grep TIME_WAIT查看
解决办法:
在bind之前尽可能调用setsockopt来设置REUSEADDR套接字选项。
使用REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器。
int on = 1; //等于1代表开启
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
9. 多个客户端(多进程)
客户端程序不变
服务器端:
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m)\
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void do_service(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int recvlen = read(conn, recvbuf, sizeof(recvbuf));
if(recvlen == 0)
{
printf("clent close\n");
break;
}
if(recvlen == -1)
{
printf("read data error\n");
break;
}
fputs(recvbuf, stdout);
write(conn, recvbuf, recvlen);
}
}
int main()
{
int listenfd;
listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
ERR_EXIT("socket");
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5788);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
int bindval = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr)
if(bindval < 0)
ERR_EXIT("bind");
if((listen(listenfd, SOMAXCONN)) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
pid_t pid;
while(1)
{
int conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn < 0)
ERR_EXIT("accept");
printf("address:%s, port:%d\n",inet_ntoa(peeraddr.sin_addr), ntohs(pe
pid = fork();
if (pid == -1)
ERR_EXIT("fork");
if (pid == 0)
{
close (listenfd);
do_service (conn);
exit (EXIT_SUCCESS); //client close,so close subprocess
//if not closed, it is will continue
//recv connect.
}
else
{
close(conn);
}
}
close(listenfd);
return 0;
}
在这里使用fork来开启一个进程, 当有一个连接过来,fork一个进程去处理客户端的连接。