这是服务器和客户端的连接流程,服务端主要进行7个步骤。分别是创建服务器socket()、设置ip和端口bind()、监听listen()、接收客户端accept()、读取数据recv()、发送数据send()、关闭服务器close()。
客户端主要进行5个步骤, 创建服务器socket()、连接服务器connect()、发送数据send()、接收数据recv()数据、关闭服务器close()。
服务端
socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
domain选项通常选这两个,这里选IPv4的AF_INET。
type
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based
byte streams. An out-of-band data transmission mecha‐
nism may be supported.
type选SOCK_STREAM这个流,protocol给0就好,让系统帮我们选择。
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1)
{
perror("socket");
exit(-1);
}
socket的返回值是int类型,这里用s_fd来接收并判断socket是否创建成功。s_fd将作为客户端的描述符。
bind
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd就是客户端的表述符。
struct sockaddr *addr
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
bind()函数的第二个参数是struct sockaddr这样一个结构体,我们用struct sockaddr_in来代替,之后用强制转换把数据类型转换回去就好了。
sin_family是协议类型,这里和socket()函数一样,选择IPv4----AF_INET。sin_port是端口号,例如8888或者5555之类的。sin_addr也是结构体定义的变量,这里代表IP地址。
struct in_addr {
__be32 s_addr;
};
两台主机想要进行网络通讯,先规定好协议类型,然后选择端口号,最后选择IP地址,这样才能通讯。一般来说,我们输入的IP地址电脑是看不懂的,这时候需要使用转换字节序inet_aton()函数,把主机字节序转换成网络字节序。同时也可以使用inet_ntoa()函数把网络字节序转换成主机字节序。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
cp是IP地址号,例如192.168.1.10等等。inp是把IP地址转换到的结构体,in是转成主机字节序的结构体。
bind()函数的最后一个参数是地址的大小。
struct sockaddr_in s_addr;
s_fd = socket(AF_INET, SOCK_STREAM, 0);
最后再介绍一下htons()函数。
#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);
介绍这个函数之前,我们要知道主机字节数有两个模式,一个是大端模式,一个是小端模式。小端模式就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。大端模式1就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
我们电脑的主机字节序一般情况下是小端的,而网络字节序是大端的,所以我们需要进行转换,不然会发生错误。htons()函数的作用就是将小端模式转换成大端模式。
bind()函数有配置的内容就三个,协议类型、端口号和IP地址,IP地址会比较麻烦。
listen
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
listen()比较简单,sockfd是服务端表述符,backlog是监听个数。
listen(s_fd,10);
accept
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd是服务端描述符。struct sockaddr *addr和bind()的是一致的,最后一个是长度,传递的是指针。
struct sockaddr_in c_addr;
int len = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&len);
accept()的返回值也是int类型,定义一个c_fd变量来接收它的返回值,也可以称为表述符。
read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd是表述符,这里比较推荐写accept()的表述符。buf是读入哪个数组,最后一个是读入的大小。
char readbuf[128];
n_read = read(c_fd,readbuf,128);
write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write()和read()没什么区别。
char *msg = "hello world";
write(c_fd,msg,strlen(msg));
close
#include <unistd.h>
int close(int fd);
close()函数也非常简单。
close(c_fd);
服务端总体代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int s_fd;
int c_fd;
int n_read;
char readbuf[128];
char *msg = "hello world";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
//socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1)
{
perror("socket");
exit(-1);
}
//bind
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8988);
inet_aton("127.0.0.1",&s_addr.sin_addr);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//listen
listen(s_fd,10);
//accept
int len = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&len);
if(c_fd == -1)
{
perror("accept");
exit(-2);
}
printf("accept success , ip = %s\n",inet_ntoa(c_addr.sin_addr));
//read
n_read = read(c_fd,readbuf,128);
if(n_read == -1)
{
perror("read");
exit(-3);
}
else
{
printf("read %d byte from client : %s\n",n_read,readbuf);
}
//write
write(c_fd,msg,strlen(msg));
close(c_fd);
return 0;
}
客户端
客户端的代码量比较少,先是创建服务器。
socket
struct sockaddr_in c_addr;
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1)
{
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8988);
inet_aton("127.0.0.1",&c_addr.sin_addr);
虽然bind()函数不需要,但是协议、端口号、IP地址还是要配置的。
connect
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()的参数和bind()的一致,搬过来用就好了。
struct sockaddr_in c_addr;
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in)) == -1)
{
perror("connect");
exit(-2);
}
write
和服务端不同的是,客户端需要先写再读。
char *msg = "hello world to";
write(c_fd,msg,strlen(msg));
read
char readbuf[128] = {0};
n_read = read(c_fd,readbuf,sizeof(readbuf));
close
close(c_fd);
客户端总代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int c_fd;
int n_read;
char readbuf[128] = {0};
char *msg = "hello world to";
struct sockaddr_in c_addr;
//socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1)
{
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8988);
inet_aton("127.0.0.1",&c_addr.sin_addr);
//connect
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in)) == -1)
{
perror("connect");
exit(-2);
}
//write
write(c_fd,msg,strlen(msg));
//read
n_read = read(c_fd,readbuf,sizeof(readbuf));
if(n_read == -1)
{
perror("read");
exit(-3);
}
else
{
printf("read %d byte from client : %s\n",n_read,readbuf);
}
close(c_fd);
return 0;
}